feat: Complete subpage redesign - all 5 issues fixed!
## ✅ All 5 Issues Resolved! ### 1. Subject in Body ✅ **Before:** Subject in sticky header **After:** Subject inside scrollable content (Editor tab) - More consistent with form patterns - Better scrolling experience - Cleaner header ### 2. Tabs Scroll-Proof ✅ **Before:** Tabs inside scrollable area **After:** Tabs sticky at top (like GitHub file viewer) ```tsx <div className="-mt-6 mb-6 sticky top-0 z-10 bg-background pb-4"> <Tabs>...</Tabs> </div> ``` - Tabs always visible while scrolling - Easy to switch Editor ↔ Preview - Professional UX ### 3. Default Values Loading ✅ **Before:** Empty editor (bad UX) **After:** Default templates load automatically **Backend Fix:** - Added `event_label` and `channel_label` to API response - Templates now load from `TemplateProvider::get_default_templates()` - Rich default content for all events **Frontend Fix:** - `useEffect` properly sets subject/body from template - RichTextEditor syncs with content prop - Preview shows actual content immediately ### 4. Page Width Matched ✅ **Before:** Custom max-w-7xl (inconsistent) **After:** Uses SettingsLayout (max-w-5xl) - Matches all other settings pages - Consistent visual width - Professional appearance ### 5. Mobile + Contextual Header ✅ **Before:** Custom header implementation **After:** Uses SettingsLayout with contextual header **Contextual Header Features:** - Title + Description in header - Back button - Reset to Default button - Save Template button (from SettingsLayout) - Mobile responsive (SettingsLayout handles it) **Mobile Strategy:** - SettingsLayout handles responsive breakpoints - Tabs stack nicely on mobile - Cards adapt to screen size - Touch-friendly buttons --- ## Architecture Changes: **Before (Dialog-like):** ``` Custom full-height layout ├── Custom sticky header ├── Subject in header ├── Tabs in body └── Custom footer ``` **After (Proper Subpage):** ``` SettingsLayout (max-w-5xl) ├── Contextual Header (sticky) │ ├── Title + Description │ └── Actions (Back, Reset, Save) ├── Sticky Tabs (scroll-proof) └── Content (scrollable) ├── Editor Tab (Card) │ ├── Subject input │ └── Rich text editor └── Preview Tab (Card) ├── Subject preview └── Email preview ``` **Benefits:** - ✅ Consistent with all settings pages - ✅ Proper contextual header - ✅ Mobile responsive - ✅ Default templates load - ✅ Scroll-proof tabs - ✅ Professional UX **Next:** Card insert buttons + Email appearance settings 🚀
This commit is contained in:
@@ -2,17 +2,27 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api';
|
||||
import { SettingsLayout } from '../components/SettingsLayout';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RichTextEditor } from '@/components/ui/rich-text-editor';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ArrowLeft, Eye, Edit, Save, RotateCcw } from 'lucide-react';
|
||||
import { ArrowLeft, Eye, Edit, RotateCcw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { __ } from '@/lib/i18n';
|
||||
|
||||
export default function EditTemplate() {
|
||||
// Mobile responsive check
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -43,40 +53,35 @@ export default function EditTemplate() {
|
||||
}
|
||||
}, [template]);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return api.post('/notifications/templates', {
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await api.post('/notifications/templates', {
|
||||
eventId,
|
||||
channelId,
|
||||
subject,
|
||||
body,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId] });
|
||||
toast.success(__('Template saved successfully'));
|
||||
navigate(-1);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || __('Failed to save template'));
|
||||
},
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const resetMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return api.del(`/notifications/templates/${eventId}/${channelId}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
const handleReset = async () => {
|
||||
if (!confirm(__('Are you sure you want to reset this template to default?'))) return;
|
||||
|
||||
try {
|
||||
await api.del(`/notifications/templates/${eventId}/${channelId}`);
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['notification-template', eventId, channelId] });
|
||||
toast.success(__('Template reset to default'));
|
||||
navigate(-1);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || __('Failed to reset template'));
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get variable keys for the rich text editor
|
||||
const variableKeys = Object.keys(variables);
|
||||
@@ -166,88 +171,49 @@ export default function EditTemplate() {
|
||||
`;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-muted-foreground">{__('Loading...')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!eventId || !channelId) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<SettingsLayout
|
||||
title={__('Edit Template')}
|
||||
description={__('Template editor')}
|
||||
>
|
||||
<div className="text-muted-foreground">{__('Invalid template parameters')}</div>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="border-b bg-background sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto w-full px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
className="gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{__('Back')}
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">
|
||||
{__('Edit Template')}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{template?.event_label} - {template?.channel_label}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{__('Customize the notification template. Use variables like {customer_name} to personalize messages.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => resetMutation.mutate()}
|
||||
disabled={saveMutation.isPending || resetMutation.isPending}
|
||||
className="gap-2"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
{__('Reset to Default')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending || resetMutation.isPending}
|
||||
className="gap-2"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{saveMutation.isPending ? __('Saving...') : __('Save Template')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsLayout
|
||||
title={template?.event_label || __('Edit Template')}
|
||||
description={`${template?.channel_label} - ${__('Customize the notification template. Use variables like {customer_name} to personalize messages.')}`}
|
||||
onSave={handleSave}
|
||||
saveLabel={__('Save Template')}
|
||||
isLoading={isLoading}
|
||||
action={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
className="gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{__('Back')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
className="gap-2"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
{__('Reset to Default')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Subject */}
|
||||
<div className="max-w-7xl mx-auto w-full px-6 pb-4">
|
||||
<Label htmlFor="subject" className="text-sm">{__('Subject / Title')}</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
placeholder={__('Enter notification subject')}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body - Tabs */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="max-w-7xl mx-auto w-full px-6 py-6">
|
||||
}
|
||||
>
|
||||
{/* Tabs - Sticky in header area */}
|
||||
<div className="-mt-6 mb-6 sticky top-0 z-10 bg-background pb-4">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full max-w-md grid-cols-2">
|
||||
<TabsTrigger value="editor" className="flex items-center gap-2">
|
||||
@@ -259,9 +225,30 @@ export default function EditTemplate() {
|
||||
{__('Preview')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Editor Tab */}
|
||||
<TabsContent value="editor" className="space-y-4 mt-6">
|
||||
{/* Editor Tab */}
|
||||
{activeTab === 'editor' && (
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-6">
|
||||
{/* Subject */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subject">{__('Subject / Title')}</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
placeholder={__('Enter notification subject')}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{channelId === 'email'
|
||||
? __('Email subject line')
|
||||
: __('Push notification title')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="body">{__('Message Body')}</Label>
|
||||
<RichTextEditor
|
||||
@@ -270,14 +257,30 @@ export default function EditTemplate() {
|
||||
placeholder={__('Enter notification message')}
|
||||
variables={variableKeys}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__('Use the toolbar to format text and insert variables.')}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Preview Tab */}
|
||||
<TabsContent value="preview" className="mt-6">
|
||||
<div className="space-y-2">
|
||||
<Label>{__('Email Preview')}</Label>
|
||||
<div className="rounded-lg border bg-white overflow-hidden">
|
||||
{/* Preview Tab */}
|
||||
{activeTab === 'preview' && (
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
{/* Subject Preview */}
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">{__('Subject')}</Label>
|
||||
<div className="mt-2 p-3 bg-muted/50 rounded-md font-medium">
|
||||
{subject || <span className="text-muted-foreground italic">{__('(No subject)')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Preview */}
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">{__('Email Preview')}</Label>
|
||||
<div className="mt-2 rounded-lg border bg-white overflow-hidden">
|
||||
<iframe
|
||||
srcDoc={generatePreviewHTML()}
|
||||
className="w-full border-0"
|
||||
@@ -292,14 +295,13 @@ export default function EditTemplate() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{__('This is how your email will look. Dynamic variables are highlighted in yellow.')}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user