feat: Enhanced Email Customization - Logo, Social, Hero Text! 🎨
## Frontend Improvements (1-3, 5) ### 1. Logo URL with WP Media Library ✅ - Added "Select" button next to logo URL input - Opens WordPress Media Library - Logo preview below input - Easier for users to select from existing media ### 2. Footer Text with {current_year} Variable ✅ - Updated placeholder to show {current_year} usage - Help text explains dynamic year variable - Backend will replace with actual year ### 3. Social Links in Footer ✅ **Platforms Supported:** - Facebook - Twitter - Instagram - LinkedIn - YouTube - Website **Features:** - Add/remove social links - Platform dropdown with icons - URL input for each link - Visual icons in UI - Will render as icons in email footer ### 5. Hero Card Text Color ✅ - Added hero_text_color field - Color picker + hex input - Applied to preview - Separate control for heading/text color - Usually white for dark gradients **Updated Interface:** ```typescript interface EmailSettings { // ... existing hero_text_color: string; social_links: SocialLink[]; } interface SocialLink { platform: string; url: string; } ``` **File:** - `routes/Settings/Notifications/EmailCustomization.tsx` Next: Wire to backend (task 4)!
This commit is contained in:
@@ -8,18 +8,27 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
import { ArrowLeft, RefreshCw, Upload, Plus, Trash2, Facebook, Twitter, Instagram, Linkedin, Youtube, Globe } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { openWPMediaLogo } from '@/lib/wp-media';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
|
interface SocialLink {
|
||||||
|
platform: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface EmailSettings {
|
interface EmailSettings {
|
||||||
primary_color: string;
|
primary_color: string;
|
||||||
secondary_color: string;
|
secondary_color: string;
|
||||||
hero_gradient_start: string;
|
hero_gradient_start: string;
|
||||||
hero_gradient_end: string;
|
hero_gradient_end: string;
|
||||||
|
hero_text_color: string;
|
||||||
button_text_color: string;
|
button_text_color: string;
|
||||||
logo_url: string;
|
logo_url: string;
|
||||||
header_text: string;
|
header_text: string;
|
||||||
footer_text: string;
|
footer_text: string;
|
||||||
|
social_links: SocialLink[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmailCustomization() {
|
export default function EmailCustomization() {
|
||||||
@@ -35,10 +44,12 @@ export default function EmailCustomization() {
|
|||||||
secondary_color: '#7f54b3',
|
secondary_color: '#7f54b3',
|
||||||
hero_gradient_start: '#667eea',
|
hero_gradient_start: '#667eea',
|
||||||
hero_gradient_end: '#764ba2',
|
hero_gradient_end: '#764ba2',
|
||||||
|
hero_text_color: '#ffffff',
|
||||||
button_text_color: '#ffffff',
|
button_text_color: '#ffffff',
|
||||||
logo_url: '',
|
logo_url: '',
|
||||||
header_text: '',
|
header_text: '',
|
||||||
footer_text: '',
|
footer_text: '',
|
||||||
|
social_links: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,10 +58,12 @@ export default function EmailCustomization() {
|
|||||||
secondary_color: '#7f54b3',
|
secondary_color: '#7f54b3',
|
||||||
hero_gradient_start: '#667eea',
|
hero_gradient_start: '#667eea',
|
||||||
hero_gradient_end: '#764ba2',
|
hero_gradient_end: '#764ba2',
|
||||||
|
hero_text_color: '#ffffff',
|
||||||
button_text_color: '#ffffff',
|
button_text_color: '#ffffff',
|
||||||
logo_url: '',
|
logo_url: '',
|
||||||
header_text: '',
|
header_text: '',
|
||||||
footer_text: '',
|
footer_text: '',
|
||||||
|
social_links: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update form when settings load
|
// Update form when settings load
|
||||||
@@ -104,6 +117,50 @@ export default function EmailCustomization() {
|
|||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogoSelect = () => {
|
||||||
|
openWPMediaLogo((media) => {
|
||||||
|
if (media && media.url) {
|
||||||
|
handleChange('logo_url', media.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSocialLink = () => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
social_links: [...prev.social_links, { platform: 'facebook', url: '' }],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSocialLink = (index: number) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
social_links: prev.social_links.filter((_, i) => i !== index),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSocialLink = (index: number, field: 'platform' | 'url', value: string) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
social_links: prev.social_links.map((link, i) =>
|
||||||
|
i === index ? { ...link, [field]: value } : link
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSocialIcon = (platform: string) => {
|
||||||
|
const icons: Record<string, any> = {
|
||||||
|
facebook: Facebook,
|
||||||
|
twitter: Twitter,
|
||||||
|
instagram: Instagram,
|
||||||
|
linkedin: Linkedin,
|
||||||
|
youtube: Youtube,
|
||||||
|
website: Globe,
|
||||||
|
};
|
||||||
|
const Icon = icons[platform] || Globe;
|
||||||
|
return <Icon className="h-4 w-4" />;
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SettingsLayout
|
<SettingsLayout
|
||||||
@@ -252,9 +309,33 @@ export default function EmailCustomization() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="hero_text_color">{__('Text Color')}</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="hero_text_color"
|
||||||
|
type="color"
|
||||||
|
value={formData.hero_text_color}
|
||||||
|
onChange={(e) => handleChange('hero_text_color', e.target.value)}
|
||||||
|
className="w-20 h-10 p-1 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={formData.hero_text_color}
|
||||||
|
onChange={(e) => handleChange('hero_text_color', e.target.value)}
|
||||||
|
placeholder="#ffffff"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{__('Text and heading color for hero cards (usually white)')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Preview */}
|
{/* Preview */}
|
||||||
<div className="mt-4 p-6 rounded-lg text-white text-center" style={{
|
<div className="mt-4 p-6 rounded-lg text-center" style={{
|
||||||
background: `linear-gradient(135deg, ${formData.hero_gradient_start} 0%, ${formData.hero_gradient_end} 100%)`
|
background: `linear-gradient(135deg, ${formData.hero_gradient_start} 0%, ${formData.hero_gradient_end} 100%)`,
|
||||||
|
color: formData.hero_text_color
|
||||||
}}>
|
}}>
|
||||||
<h3 className="text-xl font-bold mb-2">{__('Preview')}</h3>
|
<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>
|
<p className="text-sm opacity-90">{__('This is how your hero cards will look')}</p>
|
||||||
@@ -323,16 +404,37 @@ export default function EmailCustomization() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="logo_url">{__('Logo URL')}</Label>
|
<Label htmlFor="logo_url">{__('Logo URL')}</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="logo_url"
|
id="logo_url"
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.logo_url}
|
value={formData.logo_url}
|
||||||
onChange={(e) => handleChange('logo_url', e.target.value)}
|
onChange={(e) => handleChange('logo_url', e.target.value)}
|
||||||
placeholder="https://example.com/logo.png"
|
placeholder="https://example.com/logo.png"
|
||||||
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleLogoSelect}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
{__('Select')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{__('Full URL to your logo image (recommended: 200x60px)')}
|
{__('Full URL to your logo image (recommended: 200x60px)')}
|
||||||
</p>
|
</p>
|
||||||
|
{formData.logo_url && (
|
||||||
|
<div className="mt-2 p-4 border rounded-lg bg-muted/30">
|
||||||
|
<img
|
||||||
|
src={formData.logo_url}
|
||||||
|
alt="Logo preview"
|
||||||
|
className="max-h-16 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -356,10 +458,112 @@ export default function EmailCustomization() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={formData.footer_text}
|
value={formData.footer_text}
|
||||||
onChange={(e) => handleChange('footer_text', e.target.value)}
|
onChange={(e) => handleChange('footer_text', e.target.value)}
|
||||||
placeholder={__('© 2024 Your Store. All rights reserved.')}
|
placeholder={__('© {current_year} Your Store. All rights reserved.')}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{__('Text shown in email footer (copyright, address, etc.)')}
|
{__('Text shown in email footer. Use {current_year} for dynamic year.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social Links */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>{__('Social Links')}</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addSocialLink}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
{__('Add Social Link')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.social_links.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{__('No social links added. Click "Add Social Link" to get started.')}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{formData.social_links.map((link, index) => (
|
||||||
|
<div key={index} className="flex gap-2 items-start p-3 border rounded-lg">
|
||||||
|
<div className="flex-1 grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
value={link.platform}
|
||||||
|
onValueChange={(value) => updateSocialLink(index, 'platform', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getSocialIcon(link.platform)}
|
||||||
|
<SelectValue />
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="facebook">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Facebook className="h-4 w-4" />
|
||||||
|
Facebook
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="twitter">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Twitter className="h-4 w-4" />
|
||||||
|
Twitter
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="instagram">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Instagram className="h-4 w-4" />
|
||||||
|
Instagram
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="linkedin">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Linkedin className="h-4 w-4" />
|
||||||
|
LinkedIn
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="youtube">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Youtube className="h-4 w-4" />
|
||||||
|
YouTube
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="website">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
Website
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
value={link.url}
|
||||||
|
onChange={(e) => updateSocialLink(index, 'url', e.target.value)}
|
||||||
|
placeholder="https://..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeSocialLink(index)}
|
||||||
|
className="h-9 w-9 p-0 text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{__('Social links will appear as icons in the email footer')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user