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:
dwindown
2025-11-13 00:11:16 +07:00
parent 8c834bdfcc
commit 5097f4b09a
2 changed files with 134 additions and 110 deletions

View File

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