feat: Email Global Customization Page! 🎨
## 3. Email Global Customization
**Features:**
- Brand Colors (Primary & Secondary)
- Hero Card Gradient (Start & End colors)
- Button Styling (Text color)
- Logo & Branding (Logo URL, Header/Footer text)
- Live color previews
- Reset to defaults
**Settings:**
- `primary_color` - Primary buttons (#7f54b3)
- `secondary_color` - Outline buttons (#7f54b3)
- `hero_gradient_start` - Hero card gradient start (#667eea)
- `hero_gradient_end` - Hero card gradient end (#764ba2)
- `button_text_color` - Button text (#ffffff)
- `logo_url` - Store logo URL
- `header_text` - Email header text
- `footer_text` - Email footer text
**UI Features:**
- Color pickers with hex input
- Live gradient preview
- Live button preview
- Back navigation
- Reset to defaults button
- Save/loading states
**Navigation:**
- Added card to Notifications page
- Route: `/settings/notifications/email-customization`
- API: `/notifications/email-settings`
**Files:**
- `routes/Settings/Notifications.tsx` - Added card
- `routes/Settings/Notifications/EmailCustomization.tsx` - NEW
- `App.tsx` - Added route
Ready to apply these settings to email templates! 🚀
This commit is contained in:
@@ -203,6 +203,7 @@ import SettingsLocalPickup from '@/routes/Settings/LocalPickup';
|
||||
import SettingsNotifications from '@/routes/Settings/Notifications';
|
||||
import StaffNotifications from '@/routes/Settings/Notifications/Staff';
|
||||
import CustomerNotifications from '@/routes/Settings/Notifications/Customer';
|
||||
import EmailCustomization from '@/routes/Settings/Notifications/EmailCustomization';
|
||||
import EditTemplate from '@/routes/Settings/Notifications/EditTemplate';
|
||||
import SettingsDeveloper from '@/routes/Settings/Developer';
|
||||
import MorePage from '@/routes/More';
|
||||
@@ -493,6 +494,7 @@ function AppRoutes() {
|
||||
<Route path="/settings/notifications" element={<SettingsNotifications />} />
|
||||
<Route path="/settings/notifications/staff" element={<StaffNotifications />} />
|
||||
<Route path="/settings/notifications/customer" element={<CustomerNotifications />} />
|
||||
<Route path="/settings/notifications/email-customization" element={<EmailCustomization />} />
|
||||
<Route path="/settings/notifications/edit-template" element={<EditTemplate />} />
|
||||
<Route path="/settings/brand" element={<SettingsIndex />} />
|
||||
<Route path="/settings/developer" element={<SettingsDeveloper />} />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SettingsLayout } from './components/SettingsLayout';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { __ } from '@/lib/i18n';
|
||||
import { Bell, Users, ChevronRight, Activity } from 'lucide-react';
|
||||
import { Bell, Users, ChevronRight, Activity, Palette } from 'lucide-react';
|
||||
|
||||
export default function NotificationsSettings() {
|
||||
return (
|
||||
@@ -80,6 +80,39 @@ export default function NotificationsSettings() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Email Customization */}
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-green-500/10 rounded-lg">
|
||||
<Palette className="h-6 w-6 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>{__('Email Customization')}</CardTitle>
|
||||
<CardDescription>
|
||||
{__('Customize email appearance and branding')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{__('Set your brand colors, logo, and email styling. Customize header, footer, and button colors for all email templates.')}
|
||||
</p>
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{__('Colors, Logo, Styling')}
|
||||
</div>
|
||||
<Link to="/settings/notifications/email-customization">
|
||||
<Button variant="outline" size="sm">
|
||||
{__('Customize')}
|
||||
<ChevronRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Activity Log */}
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
<CardHeader>
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api';
|
||||
import { SettingsLayout } from '../components/SettingsLayout';
|
||||
import { SettingsCard } from '../components/SettingsCard';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { __ } from '@/lib/i18n';
|
||||
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface EmailSettings {
|
||||
primary_color: string;
|
||||
secondary_color: string;
|
||||
hero_gradient_start: string;
|
||||
hero_gradient_end: string;
|
||||
button_text_color: string;
|
||||
logo_url: string;
|
||||
header_text: string;
|
||||
footer_text: string;
|
||||
}
|
||||
|
||||
export default function EmailCustomization() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch email settings
|
||||
const { data: settings, isLoading } = useQuery({
|
||||
queryKey: ['email-settings'],
|
||||
queryFn: () => api.get('/notifications/email-settings'),
|
||||
placeholderData: {
|
||||
primary_color: '#7f54b3',
|
||||
secondary_color: '#7f54b3',
|
||||
hero_gradient_start: '#667eea',
|
||||
hero_gradient_end: '#764ba2',
|
||||
button_text_color: '#ffffff',
|
||||
logo_url: '',
|
||||
header_text: '',
|
||||
footer_text: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState<EmailSettings>(settings || {
|
||||
primary_color: '#7f54b3',
|
||||
secondary_color: '#7f54b3',
|
||||
hero_gradient_start: '#667eea',
|
||||
hero_gradient_end: '#764ba2',
|
||||
button_text_color: '#ffffff',
|
||||
logo_url: '',
|
||||
header_text: '',
|
||||
footer_text: '',
|
||||
});
|
||||
|
||||
// Update form when settings load
|
||||
React.useEffect(() => {
|
||||
if (settings) {
|
||||
setFormData(settings);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return api.post('/notifications/email-settings', formData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['email-settings'] });
|
||||
toast.success(__('Email settings saved successfully'));
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.message || __('Failed to save email settings'));
|
||||
},
|
||||
});
|
||||
|
||||
const resetMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return api.del('/notifications/email-settings');
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['email-settings'] });
|
||||
toast.success(__('Email settings reset to defaults'));
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.message || __('Failed to reset email settings'));
|
||||
},
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
saveMutation.mutate(undefined, {
|
||||
onSuccess: () => resolve(),
|
||||
onError: () => reject(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (!confirm(__('Are you sure you want to reset all email customization to defaults?'))) return;
|
||||
resetMutation.mutate();
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof EmailSettings, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SettingsLayout
|
||||
title={__('Email Customization')}
|
||||
description={__('Loading...')}
|
||||
>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
title={__('Email Customization')}
|
||||
description={__('Customize the appearance and branding of all email templates')}
|
||||
onSave={handleSave}
|
||||
saveLabel={__('Save Settings')}
|
||||
isLoading={saveMutation.isPending}
|
||||
action={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate('/settings/notifications')}
|
||||
className="gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{__('Back')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
disabled={resetMutation.isPending}
|
||||
>
|
||||
{resetMutation.isPending ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
__('Reset to Defaults')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Brand Colors */}
|
||||
<SettingsCard
|
||||
title={__('Brand Colors')}
|
||||
description={__('Set your primary and secondary brand colors for buttons and accents')}
|
||||
>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="primary_color">{__('Primary Color')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="primary_color"
|
||||
type="color"
|
||||
value={formData.primary_color}
|
||||
onChange={(e) => handleChange('primary_color', e.target.value)}
|
||||
className="w-20 h-10 p-1 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.primary_color}
|
||||
onChange={(e) => handleChange('primary_color', e.target.value)}
|
||||
placeholder="#7f54b3"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__('Used for primary buttons and main accents')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="secondary_color">{__('Secondary Color')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="secondary_color"
|
||||
type="color"
|
||||
value={formData.secondary_color}
|
||||
onChange={(e) => handleChange('secondary_color', e.target.value)}
|
||||
className="w-20 h-10 p-1 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.secondary_color}
|
||||
onChange={(e) => handleChange('secondary_color', e.target.value)}
|
||||
placeholder="#7f54b3"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__('Used for outline buttons and borders')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Hero Card Gradient */}
|
||||
<SettingsCard
|
||||
title={__('Hero Card Gradient')}
|
||||
description={__('Customize the gradient colors for hero/success card backgrounds')}
|
||||
>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hero_gradient_start">{__('Gradient Start')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="hero_gradient_start"
|
||||
type="color"
|
||||
value={formData.hero_gradient_start}
|
||||
onChange={(e) => handleChange('hero_gradient_start', e.target.value)}
|
||||
className="w-20 h-10 p-1 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.hero_gradient_start}
|
||||
onChange={(e) => handleChange('hero_gradient_start', e.target.value)}
|
||||
placeholder="#667eea"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hero_gradient_end">{__('Gradient End')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="hero_gradient_end"
|
||||
type="color"
|
||||
value={formData.hero_gradient_end}
|
||||
onChange={(e) => handleChange('hero_gradient_end', e.target.value)}
|
||||
className="w-20 h-10 p-1 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.hero_gradient_end}
|
||||
onChange={(e) => handleChange('hero_gradient_end', e.target.value)}
|
||||
placeholder="#764ba2"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="mt-4 p-6 rounded-lg text-white text-center" style={{
|
||||
background: `linear-gradient(135deg, ${formData.hero_gradient_start} 0%, ${formData.hero_gradient_end} 100%)`
|
||||
}}>
|
||||
<h3 className="text-xl font-bold mb-2">{__('Preview')}</h3>
|
||||
<p className="text-sm opacity-90">{__('This is how your hero cards will look')}</p>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Button Styling */}
|
||||
<SettingsCard
|
||||
title={__('Button Styling')}
|
||||
description={__('Customize button text color and appearance')}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="button_text_color">{__('Button Text Color')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="button_text_color"
|
||||
type="color"
|
||||
value={formData.button_text_color}
|
||||
onChange={(e) => handleChange('button_text_color', e.target.value)}
|
||||
className="w-20 h-10 p-1 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.button_text_color}
|
||||
onChange={(e) => handleChange('button_text_color', e.target.value)}
|
||||
placeholder="#ffffff"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__('Text color for buttons (usually white for dark buttons)')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Button Preview */}
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
<button
|
||||
className="px-6 py-3 rounded-lg font-medium"
|
||||
style={{
|
||||
backgroundColor: formData.primary_color,
|
||||
color: formData.button_text_color,
|
||||
}}
|
||||
>
|
||||
{__('Primary Button')}
|
||||
</button>
|
||||
<button
|
||||
className="px-6 py-3 rounded-lg font-medium border-2"
|
||||
style={{
|
||||
borderColor: formData.secondary_color,
|
||||
color: formData.secondary_color,
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
{__('Secondary Button')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Logo & Branding */}
|
||||
<SettingsCard
|
||||
title={__('Logo & Branding')}
|
||||
description={__('Add your logo and custom header/footer text')}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="logo_url">{__('Logo URL')}</Label>
|
||||
<Input
|
||||
id="logo_url"
|
||||
type="url"
|
||||
value={formData.logo_url}
|
||||
onChange={(e) => handleChange('logo_url', e.target.value)}
|
||||
placeholder="https://example.com/logo.png"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__('Full URL to your logo image (recommended: 200x60px)')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="header_text">{__('Header Text')}</Label>
|
||||
<Input
|
||||
id="header_text"
|
||||
type="text"
|
||||
value={formData.header_text}
|
||||
onChange={(e) => handleChange('header_text', e.target.value)}
|
||||
placeholder={__('Your Store Name')}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__('Text shown in email header (leave empty to use store name)')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="footer_text">{__('Footer Text')}</Label>
|
||||
<Input
|
||||
id="footer_text"
|
||||
type="text"
|
||||
value={formData.footer_text}
|
||||
onChange={(e) => handleChange('footer_text', e.target.value)}
|
||||
placeholder={__('© 2024 Your Store. All rights reserved.')}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__('Text shown in email footer (copyright, address, etc.)')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p className="text-sm text-blue-900 dark:text-blue-100">
|
||||
<strong>{__('Note:')}</strong> {__('These settings will apply to all email templates. Individual templates can still override specific content, but colors and branding will be consistent across all emails.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user