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:
Dwindi Ramadhana
2025-12-25 22:20:48 +07:00
parent c37ecb8e96
commit 9ac09582d2
104 changed files with 14801 additions and 1213 deletions

View 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>
);
}