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:
dwindown
2025-11-13 13:15:30 +07:00
parent 0ab08d2f09
commit 704e9942e1
3 changed files with 413 additions and 1 deletions

View File

@@ -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 />} />

View File

@@ -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>

View File

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