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:
463
admin-spa/src/routes/Appearance/Footer.tsx
Normal file
463
admin-spa/src/routes/Appearance/Footer.tsx
Normal file
@@ -0,0 +1,463 @@
|
||||
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 { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface SocialLink {
|
||||
id: string;
|
||||
platform: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface FooterSection {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'menu' | 'contact' | 'social' | 'newsletter' | 'custom';
|
||||
content: any;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface ContactData {
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
show_email: boolean;
|
||||
show_phone: boolean;
|
||||
show_address: boolean;
|
||||
}
|
||||
|
||||
export default function AppearanceFooter() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [columns, setColumns] = useState('4');
|
||||
const [style, setStyle] = useState('detailed');
|
||||
const [copyrightText, setCopyrightText] = useState('© 2024 WooNooW. All rights reserved.');
|
||||
|
||||
const [elements, setElements] = useState({
|
||||
newsletter: true,
|
||||
social: true,
|
||||
payment: true,
|
||||
copyright: true,
|
||||
menu: true,
|
||||
contact: true,
|
||||
});
|
||||
|
||||
const [socialLinks, setSocialLinks] = useState<SocialLink[]>([]);
|
||||
const [sections, setSections] = useState<FooterSection[]>([]);
|
||||
const [contactData, setContactData] = useState<ContactData>({
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
show_email: true,
|
||||
show_phone: true,
|
||||
show_address: true,
|
||||
});
|
||||
|
||||
const defaultSections: FooterSection[] = [
|
||||
{ id: '1', title: 'Contact', type: 'contact', content: '', visible: true },
|
||||
{ id: '2', title: 'Quick Links', type: 'menu', content: '', visible: true },
|
||||
{ id: '3', title: 'Follow Us', type: 'social', content: '', visible: true },
|
||||
{ id: '4', title: 'Newsletter', type: 'newsletter', content: '', visible: true },
|
||||
];
|
||||
|
||||
const [labels, setLabels] = useState({
|
||||
contact_title: 'Contact',
|
||||
menu_title: 'Quick Links',
|
||||
social_title: 'Follow Us',
|
||||
newsletter_title: 'Newsletter',
|
||||
newsletter_description: 'Subscribe to get updates',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const response = await api.get('/appearance/settings');
|
||||
const footer = response.data?.footer;
|
||||
|
||||
if (footer) {
|
||||
if (footer.columns) setColumns(footer.columns);
|
||||
if (footer.style) setStyle(footer.style);
|
||||
if (footer.copyright_text) setCopyrightText(footer.copyright_text);
|
||||
if (footer.elements) setElements(footer.elements);
|
||||
if (footer.social_links) setSocialLinks(footer.social_links);
|
||||
if (footer.sections && footer.sections.length > 0) {
|
||||
setSections(footer.sections);
|
||||
} else {
|
||||
setSections(defaultSections);
|
||||
}
|
||||
if (footer.contact_data) setContactData(footer.contact_data);
|
||||
if (footer.labels) setLabels(footer.labels);
|
||||
} else {
|
||||
setSections(defaultSections);
|
||||
}
|
||||
|
||||
// Fetch store identity data
|
||||
try {
|
||||
const identityResponse = await api.get('/settings/store-identity');
|
||||
const identity = identityResponse.data;
|
||||
if (identity && !footer?.contact_data) {
|
||||
setContactData(prev => ({
|
||||
...prev,
|
||||
email: identity.email || prev.email,
|
||||
phone: identity.phone || prev.phone,
|
||||
address: identity.address || prev.address,
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Store identity not available');
|
||||
}
|
||||
} 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 addSocialLink = () => {
|
||||
setSocialLinks([
|
||||
...socialLinks,
|
||||
{ id: Date.now().toString(), platform: '', url: '' },
|
||||
]);
|
||||
};
|
||||
|
||||
const removeSocialLink = (id: string) => {
|
||||
setSocialLinks(socialLinks.filter(link => link.id !== id));
|
||||
};
|
||||
|
||||
const updateSocialLink = (id: string, field: 'platform' | 'url', value: string) => {
|
||||
setSocialLinks(socialLinks.map(link =>
|
||||
link.id === id ? { ...link, [field]: value } : link
|
||||
));
|
||||
};
|
||||
|
||||
const addSection = () => {
|
||||
setSections([
|
||||
...sections,
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
title: 'New Section',
|
||||
type: 'custom',
|
||||
content: '',
|
||||
visible: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const removeSection = (id: string) => {
|
||||
setSections(sections.filter(s => s.id !== id));
|
||||
};
|
||||
|
||||
const updateSection = (id: string, field: keyof FooterSection, value: any) => {
|
||||
setSections(sections.map(s => s.id === id ? { ...s, [field]: value } : s));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await api.post('/appearance/footer', {
|
||||
columns,
|
||||
style,
|
||||
copyright_text: copyrightText,
|
||||
elements,
|
||||
social_links: socialLinks,
|
||||
sections,
|
||||
contact_data: contactData,
|
||||
labels,
|
||||
});
|
||||
toast.success('Footer settings saved successfully');
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
toast.error('Failed to save settings');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
title="Footer Settings"
|
||||
onSave={handleSave}
|
||||
isLoading={loading}
|
||||
>
|
||||
{/* Layout */}
|
||||
<SettingsCard
|
||||
title="Layout"
|
||||
description="Configure footer layout and style"
|
||||
>
|
||||
<SettingsSection label="Columns" htmlFor="footer-columns">
|
||||
<Select value={columns} onValueChange={setColumns}>
|
||||
<SelectTrigger id="footer-columns">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1 Column</SelectItem>
|
||||
<SelectItem value="2">2 Columns</SelectItem>
|
||||
<SelectItem value="3">3 Columns</SelectItem>
|
||||
<SelectItem value="4">4 Columns</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection label="Style" htmlFor="footer-style">
|
||||
<Select value={style} onValueChange={setStyle}>
|
||||
<SelectTrigger id="footer-style">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="simple">Simple</SelectItem>
|
||||
<SelectItem value="detailed">Detailed</SelectItem>
|
||||
<SelectItem value="minimal">Minimal</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsSection>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Labels */}
|
||||
<SettingsCard
|
||||
title="Section Labels"
|
||||
description="Customize footer section headings and text"
|
||||
>
|
||||
<SettingsSection label="Contact Title" htmlFor="contact-title">
|
||||
<Input
|
||||
id="contact-title"
|
||||
value={labels.contact_title}
|
||||
onChange={(e) => setLabels({ ...labels, contact_title: e.target.value })}
|
||||
placeholder="Contact"
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection label="Menu Title" htmlFor="menu-title">
|
||||
<Input
|
||||
id="menu-title"
|
||||
value={labels.menu_title}
|
||||
onChange={(e) => setLabels({ ...labels, menu_title: e.target.value })}
|
||||
placeholder="Quick Links"
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection label="Social Title" htmlFor="social-title">
|
||||
<Input
|
||||
id="social-title"
|
||||
value={labels.social_title}
|
||||
onChange={(e) => setLabels({ ...labels, social_title: e.target.value })}
|
||||
placeholder="Follow Us"
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection label="Newsletter Title" htmlFor="newsletter-title">
|
||||
<Input
|
||||
id="newsletter-title"
|
||||
value={labels.newsletter_title}
|
||||
onChange={(e) => setLabels({ ...labels, newsletter_title: e.target.value })}
|
||||
placeholder="Newsletter"
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection label="Newsletter Description" htmlFor="newsletter-desc">
|
||||
<Input
|
||||
id="newsletter-desc"
|
||||
value={labels.newsletter_description}
|
||||
onChange={(e) => setLabels({ ...labels, newsletter_description: e.target.value })}
|
||||
placeholder="Subscribe to get updates"
|
||||
/>
|
||||
</SettingsSection>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Contact Data */}
|
||||
<SettingsCard
|
||||
title="Contact Information"
|
||||
description="Manage contact details from Store Identity"
|
||||
>
|
||||
<SettingsSection label="Email" htmlFor="contact-email">
|
||||
<Input
|
||||
id="contact-email"
|
||||
type="email"
|
||||
value={contactData.email}
|
||||
onChange={(e) => setContactData({ ...contactData, email: e.target.value })}
|
||||
placeholder="info@store.com"
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Switch
|
||||
checked={contactData.show_email}
|
||||
onCheckedChange={(checked) => setContactData({ ...contactData, show_email: checked })}
|
||||
/>
|
||||
<Label className="text-sm text-muted-foreground">Show in footer</Label>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection label="Phone" htmlFor="contact-phone">
|
||||
<Input
|
||||
id="contact-phone"
|
||||
type="tel"
|
||||
value={contactData.phone}
|
||||
onChange={(e) => setContactData({ ...contactData, phone: e.target.value })}
|
||||
placeholder="(123) 456-7890"
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Switch
|
||||
checked={contactData.show_phone}
|
||||
onCheckedChange={(checked) => setContactData({ ...contactData, show_phone: checked })}
|
||||
/>
|
||||
<Label className="text-sm text-muted-foreground">Show in footer</Label>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection label="Address" htmlFor="contact-address">
|
||||
<Textarea
|
||||
id="contact-address"
|
||||
value={contactData.address}
|
||||
onChange={(e) => setContactData({ ...contactData, address: e.target.value })}
|
||||
placeholder="123 Main St, City, State 12345"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Switch
|
||||
checked={contactData.show_address}
|
||||
onCheckedChange={(checked) => setContactData({ ...contactData, show_address: checked })}
|
||||
/>
|
||||
<Label className="text-sm text-muted-foreground">Show in footer</Label>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Content */}
|
||||
<SettingsCard
|
||||
title="Content"
|
||||
description="Customize footer content"
|
||||
>
|
||||
<SettingsSection label="Copyright Text" htmlFor="copyright">
|
||||
<Textarea
|
||||
id="copyright"
|
||||
value={copyrightText}
|
||||
onChange={(e) => setCopyrightText(e.target.value)}
|
||||
rows={2}
|
||||
placeholder="© 2024 Your Store. All rights reserved."
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Social Media Links</Label>
|
||||
<Button onClick={addSocialLink} variant="outline" size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Link
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{socialLinks.map((link) => (
|
||||
<div key={link.id} className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Platform (e.g., Facebook)"
|
||||
value={link.platform}
|
||||
onChange={(e) => updateSocialLink(link.id, 'platform', e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
placeholder="URL"
|
||||
value={link.url}
|
||||
onChange={(e) => updateSocialLink(link.id, 'url', e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => removeSocialLink(link.id)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Custom Sections Builder */}
|
||||
<SettingsCard
|
||||
title="Custom Sections"
|
||||
description="Build custom footer sections with flexible content"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Footer Sections</Label>
|
||||
<Button onClick={addSection} variant="outline" size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Section
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{sections.map((section) => (
|
||||
<div key={section.id} className="border rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Input
|
||||
placeholder="Section Title"
|
||||
value={section.title}
|
||||
onChange={(e) => updateSection(section.id, 'title', e.target.value)}
|
||||
className="flex-1 mr-2"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => removeSection(section.id)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={section.type}
|
||||
onValueChange={(value) => updateSection(section.id, 'type', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="menu">Menu Links</SelectItem>
|
||||
<SelectItem value="contact">Contact Info</SelectItem>
|
||||
<SelectItem value="social">Social Links</SelectItem>
|
||||
<SelectItem value="newsletter">Newsletter Form</SelectItem>
|
||||
<SelectItem value="custom">Custom HTML</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{section.type === 'custom' && (
|
||||
<Textarea
|
||||
placeholder="Custom content (HTML supported)"
|
||||
value={section.content}
|
||||
onChange={(e) => updateSection(section.id, 'content', e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={section.visible}
|
||||
onCheckedChange={(checked) => updateSection(section.id, 'visible', checked)}
|
||||
/>
|
||||
<Label className="text-sm text-muted-foreground">Visible</Label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{sections.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No custom sections yet. Click "Add Section" to create one.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user