From 5097f4b09a77fbaa835bcdc060af34997be3767d Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 13 Nov 2025 00:11:16 +0700 Subject: [PATCH] feat: Complete subpage redesign - all 5 issues fixed! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ✅ 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
...
``` - 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 🚀 --- .../Settings/Notifications/EditTemplate.tsx | 222 +++++++++--------- includes/Api/NotificationsController.php | 22 ++ 2 files changed, 134 insertions(+), 110 deletions(-) diff --git a/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx b/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx index cacdf98..d1f8f48 100644 --- a/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx +++ b/admin-spa/src/routes/Settings/Notifications/EditTemplate.tsx @@ -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 ( -
-
{__('Loading...')}
-
- ); - } - if (!eventId || !channelId) { return ( -
+
{__('Invalid template parameters')}
-
+ ); } return ( -
- {/* Header */} -
-
-
-
- -
-

- {__('Edit Template')} -

-

- {template?.event_label} - {template?.channel_label} -

-

- {__('Customize the notification template. Use variables like {customer_name} to personalize messages.')} -

-
-
-
- - -
-
+ + +
- - {/* Subject */} -
- - setSubject(e.target.value)} - placeholder={__('Enter notification subject')} - className="mt-2" - /> -
-
- - {/* Body - Tabs */} -
-
+ } + > + {/* Tabs - Sticky in header area */} +
@@ -259,9 +225,30 @@ export default function EditTemplate() { {__('Preview')} + +
- {/* Editor Tab */} - + {/* Editor Tab */} + {activeTab === 'editor' && ( + + + {/* Subject */} +
+ + setSubject(e.target.value)} + placeholder={__('Enter notification subject')} + /> +

+ {channelId === 'email' + ? __('Email subject line') + : __('Push notification title')} +

+
+ + {/* Body */}
+

+ {__('Use the toolbar to format text and insert variables.')} +

-
+ + + )} - {/* Preview Tab */} - -
- -
+ {/* Preview Tab */} + {activeTab === 'preview' && ( + + + {/* Subject Preview */} +
+ +
+ {subject || {__('(No subject)')}} +
+
+ + {/* Email Preview */} +
+ +