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:
280
admin-spa/src/routes/Appearance/General.tsx
Normal file
280
admin-spa/src/routes/Appearance/General.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
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 { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
export default function AppearanceGeneral() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [spaMode, setSpaMode] = useState<'disabled' | 'checkout_only' | 'full'>('full');
|
||||
const [typographyMode, setTypographyMode] = useState<'predefined' | 'custom_google'>('predefined');
|
||||
const [predefinedPair, setPredefinedPair] = useState('modern');
|
||||
const [customHeading, setCustomHeading] = useState('');
|
||||
const [customBody, setCustomBody] = useState('');
|
||||
const [fontScale, setFontScale] = useState([1.0]);
|
||||
|
||||
const fontPairs = {
|
||||
modern: { name: 'Modern & Clean', fonts: 'Inter' },
|
||||
editorial: { name: 'Editorial', fonts: 'Playfair Display + Source Sans' },
|
||||
friendly: { name: 'Friendly', fonts: 'Poppins + Open Sans' },
|
||||
elegant: { name: 'Elegant', fonts: 'Cormorant + Lato' },
|
||||
};
|
||||
|
||||
const [colors, setColors] = useState({
|
||||
primary: '#1a1a1a',
|
||||
secondary: '#6b7280',
|
||||
accent: '#3b82f6',
|
||||
text: '#111827',
|
||||
background: '#ffffff',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const response = await api.get('/appearance/settings');
|
||||
const general = response.data?.general;
|
||||
|
||||
if (general) {
|
||||
if (general.spa_mode) setSpaMode(general.spa_mode);
|
||||
if (general.typography) {
|
||||
setTypographyMode(general.typography.mode || 'predefined');
|
||||
setPredefinedPair(general.typography.predefined_pair || 'modern');
|
||||
setCustomHeading(general.typography.custom?.heading || '');
|
||||
setCustomBody(general.typography.custom?.body || '');
|
||||
setFontScale([general.typography.scale || 1.0]);
|
||||
}
|
||||
if (general.colors) {
|
||||
setColors({
|
||||
primary: general.colors.primary || '#1a1a1a',
|
||||
secondary: general.colors.secondary || '#6b7280',
|
||||
accent: general.colors.accent || '#3b82f6',
|
||||
text: general.colors.text || '#111827',
|
||||
background: general.colors.background || '#ffffff',
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await api.post('/appearance/general', {
|
||||
spa_mode: spaMode,
|
||||
typography: {
|
||||
mode: typographyMode,
|
||||
predefined_pair: typographyMode === 'predefined' ? predefinedPair : undefined,
|
||||
custom: typographyMode === 'custom_google' ? { heading: customHeading, body: customBody } : undefined,
|
||||
scale: fontScale[0],
|
||||
},
|
||||
colors,
|
||||
});
|
||||
|
||||
toast.success('General settings saved successfully');
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
toast.error('Failed to save settings');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
title="General Settings"
|
||||
onSave={handleSave}
|
||||
isLoading={loading}
|
||||
>
|
||||
{/* SPA Mode */}
|
||||
<SettingsCard
|
||||
title="SPA Mode"
|
||||
description="Choose how the Single Page Application is implemented"
|
||||
>
|
||||
<RadioGroup value={spaMode} onValueChange={(value: any) => setSpaMode(value)}>
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="disabled" id="spa-disabled" />
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="spa-disabled" className="font-medium cursor-pointer">
|
||||
Disabled
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use WordPress default pages (no SPA functionality)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="checkout_only" id="spa-checkout" />
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="spa-checkout" className="font-medium cursor-pointer">
|
||||
Checkout Only
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
SPA for checkout flow only (cart, checkout, thank you)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="full" id="spa-full" />
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="spa-full" className="font-medium cursor-pointer">
|
||||
Full SPA
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Entire customer-facing site uses SPA (recommended)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Typography */}
|
||||
<SettingsCard
|
||||
title="Typography"
|
||||
description="Choose fonts for your store"
|
||||
>
|
||||
<RadioGroup value={typographyMode} onValueChange={(value: any) => setTypographyMode(value)}>
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="predefined" id="typo-predefined" />
|
||||
<div className="space-y-1 flex-1">
|
||||
<Label htmlFor="typo-predefined" className="font-medium cursor-pointer">
|
||||
Predefined Font Pairs (GDPR-compliant)
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Self-hosted fonts, no external requests
|
||||
</p>
|
||||
|
||||
{typographyMode === 'predefined' && (
|
||||
<Select value={predefinedPair} onValueChange={setPredefinedPair}>
|
||||
<SelectTrigger className="w-full min-w-[300px] [&>span]:line-clamp-none [&>span]:whitespace-normal">
|
||||
<SelectValue>
|
||||
{fontPairs[predefinedPair as keyof typeof fontPairs]?.name}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="modern">
|
||||
<div>
|
||||
<div className="font-medium">Modern & Clean</div>
|
||||
<div className="text-xs text-muted-foreground">Inter</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="editorial">
|
||||
<div>
|
||||
<div className="font-medium">Editorial</div>
|
||||
<div className="text-xs text-muted-foreground">Playfair Display + Source Sans</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="friendly">
|
||||
<div>
|
||||
<div className="font-medium">Friendly</div>
|
||||
<div className="text-xs text-muted-foreground">Poppins + Open Sans</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="elegant">
|
||||
<div>
|
||||
<div className="font-medium">Elegant</div>
|
||||
<div className="text-xs text-muted-foreground">Cormorant + Lato</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="custom_google" id="typo-custom" />
|
||||
<div className="space-y-1 flex-1">
|
||||
<Label htmlFor="typo-custom" className="font-medium cursor-pointer">
|
||||
Custom Google Fonts
|
||||
</Label>
|
||||
<Alert className="mt-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Using Google Fonts may not be GDPR compliant
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{typographyMode === 'custom_google' && (
|
||||
<div className="space-y-3 mt-3">
|
||||
<SettingsSection label="Heading Font" htmlFor="heading-font">
|
||||
<Input
|
||||
id="heading-font"
|
||||
placeholder="e.g., Montserrat"
|
||||
value={customHeading}
|
||||
onChange={(e) => setCustomHeading(e.target.value)}
|
||||
/>
|
||||
</SettingsSection>
|
||||
<SettingsSection label="Body Font" htmlFor="body-font">
|
||||
<Input
|
||||
id="body-font"
|
||||
placeholder="e.g., Roboto"
|
||||
value={customBody}
|
||||
onChange={(e) => setCustomBody(e.target.value)}
|
||||
/>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
<div className="space-y-3 pt-4 border-t">
|
||||
<Label>Font Scale: {fontScale[0].toFixed(1)}x</Label>
|
||||
<Slider
|
||||
value={fontScale}
|
||||
onValueChange={setFontScale}
|
||||
min={0.8}
|
||||
max={1.2}
|
||||
step={0.1}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Adjust the overall size of all text (0.8x - 1.2x)
|
||||
</p>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Colors */}
|
||||
<SettingsCard
|
||||
title="Colors"
|
||||
description="Customize your store's color palette"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Object.entries(colors).map(([key, value]) => (
|
||||
<SettingsSection key={key} label={key.charAt(0).toUpperCase() + key.slice(1)} htmlFor={`color-${key}`}>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id={`color-${key}`}
|
||||
type="color"
|
||||
value={value}
|
||||
onChange={(e) => setColors({ ...colors, [key]: e.target.value })}
|
||||
className="w-20 h-10 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setColors({ ...colors, [key]: e.target.value })}
|
||||
className="flex-1 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
))}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user