From 97335f265001ca8ee3c115381617fbe14faea0d5 Mon Sep 17 00:00:00 2001 From: brooktewabe Date: Sat, 6 Jun 2026 15:07:58 +0300 Subject: [PATCH] email templates and subs system --- src/App.tsx | 15 ++ src/config/admin-search-routes.ts | 23 +- src/layouts/app-shell.tsx | 78 +++++- src/pages/admin/email-templates/[key].tsx | 94 +++++++ src/pages/admin/email-templates/index.tsx | 82 ++++++ src/pages/admin/subscriptions/index.tsx | 105 ++++++++ src/pages/admin/subscriptions/plans/[id].tsx | 250 ++++++++++++++++++ .../subscription-transactions.tsx | 14 +- src/pages/admin/users/[id]/index.tsx | 176 +++++++++++- src/services/email.service.ts | 26 ++ src/services/index.ts | 17 ++ .../subscription-transaction.service.ts | 4 +- src/services/subscription.service.ts | 67 +++++ src/types/email.types.ts | 18 ++ src/types/subscription.types.ts | 117 ++++++++ 15 files changed, 1060 insertions(+), 26 deletions(-) create mode 100644 src/pages/admin/email-templates/[key].tsx create mode 100644 src/pages/admin/email-templates/index.tsx create mode 100644 src/pages/admin/subscriptions/index.tsx create mode 100644 src/pages/admin/subscriptions/plans/[id].tsx create mode 100644 src/services/email.service.ts create mode 100644 src/services/subscription.service.ts create mode 100644 src/types/email.types.ts create mode 100644 src/types/subscription.types.ts diff --git a/src/App.tsx b/src/App.tsx index 8383a30..b61c103 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,10 @@ import SystemMembersPage from "@/pages/admin/system-members"; import IssuesPage from "@/pages/admin/issues"; import FaqSupportPage from "@/pages/admin/support/faq"; import NotificationBroadcastPage from "@/pages/admin/notifications/broadcast"; +import EmailTemplatesPage from "@/pages/admin/email-templates"; +import EmailTemplatePreviewPage from "@/pages/admin/email-templates/[key]"; +import SubscriptionsAdminPage from "@/pages/admin/subscriptions"; +import PlanManagementPage from "@/pages/admin/subscriptions/plans/[id]"; function App() { return ( @@ -110,6 +114,17 @@ function App() { element={} /> } /> + } /> + } + /> + } /> + } + /> + } /> } /> diff --git a/src/config/admin-search-routes.ts b/src/config/admin-search-routes.ts index 6296288..f3d93d7 100644 --- a/src/config/admin-search-routes.ts +++ b/src/config/admin-search-routes.ts @@ -26,6 +26,8 @@ import { LifeBuoy, HelpCircle, Send, + Mail, + Layers, } from "lucide-react" export interface AdminSearchRoute { @@ -103,9 +105,27 @@ export const ADMIN_SEARCH_ROUTES: AdminSearchRoute[] = [ title: "Subscription transactions", description: "Successful and failed subscription charges for the platform.", - group: "Commerce", + group: "Subscriptions", icon: ArrowRightLeft, }, + { + id: "subscription-plans", + path: "/admin/subscriptions", + title: "Subscription plans", + description: + "Manage plan pricing, feature flags, limits, and activation.", + group: "Subscriptions", + icon: Layers, + }, + { + id: "email-templates", + path: "/admin/email-templates", + title: "Email templates", + description: + "Preview transactional email templates with sample data.", + group: "Communications", + icon: Mail, + }, { id: "users", path: "/admin/users", @@ -299,6 +319,7 @@ export const ADMIN_SEARCH_ROUTES: AdminSearchRoute[] = [ const GROUP_ORDER = [ "Overview", "Commerce", + "Subscriptions", "People & activity", "Support", "Operations", diff --git a/src/layouts/app-shell.tsx b/src/layouts/app-shell.tsx index 8351bb2..4dbe40c 100644 --- a/src/layouts/app-shell.tsx +++ b/src/layouts/app-shell.tsx @@ -24,6 +24,8 @@ import { ChevronDown, ChevronRight, Folder, + Mail, + Layers, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { AdminQuickSearch } from "@/components/admin-quick-search"; @@ -56,6 +58,31 @@ type NavItem = { visible?: (role: string | undefined) => boolean; }; +function navItemIsActive( + item: NavItem, + isActive: (path?: string) => boolean, +): boolean { + if (item.path && isActive(item.path)) return true; + return item.children?.some((child) => navItemIsActive(child, isActive)) ?? false; +} + +function filterNavItems( + items: NavItem[], + role: string | undefined, +): NavItem[] { + return items + .filter((item) => (item.visible ? item.visible(role) : true)) + .map((item) => + item.children + ? { + ...item, + children: filterNavItems(item.children, role), + } + : item, + ) + .filter((item) => !item.children || item.children.length > 0); +} + const adminNavigationItems: NavItem[] = [ { icon: LayoutDashboard, label: "Dashboard", path: "/admin/dashboard" }, { @@ -91,8 +118,19 @@ const adminNavigationItems: NavItem[] = [ }, { icon: ArrowRightLeft, - label: "Subscription transactions", - path: "/admin/transactions/subscriptions", + label: "Subscriptions", + children: [ + { + label: "Transactions", + path: "/admin/transactions/subscriptions", + icon: ArrowRightLeft, + }, + { + label: "Plans", + path: "/admin/subscriptions", + icon: Layers, + }, + ], }, { icon: Users, @@ -153,9 +191,20 @@ const adminNavigationItems: NavItem[] = [ }, { icon: Send, - label: "Send notification", - path: "/admin/notifications/broadcast", - visible: (role) => getPermissions(role).canSendNotifications, + label: "Communications", + children: [ + { + label: "Send notification", + path: "/admin/notifications/broadcast", + icon: Send, + visible: (role) => getPermissions(role).canSendNotifications, + }, + { + label: "Email templates", + path: "/admin/email-templates", + icon: Mail, + }, + ], }, ]; @@ -163,15 +212,18 @@ const SidebarNavItem = ({ item, depth = 0, isActive, + role, }: { item: NavItem; depth?: number; isActive: (path?: string) => boolean; + role: string | undefined; }) => { - const hasChildren = item.children && item.children.length > 0; - const isCurrentlyActive = - isActive(item.path) || - (hasChildren && item.children?.some((child) => isActive(child.path))); + const visibleChildren = item.children + ? filterNavItems(item.children, role) + : undefined; + const hasChildren = visibleChildren && visibleChildren.length > 0; + const isCurrentlyActive = navItemIsActive(item, isActive); const [isOpen, setIsOpen] = useState(isCurrentlyActive); @@ -209,12 +261,13 @@ const SidebarNavItem = ({ {isOpen && (
- {item.children?.map((child) => ( + {visibleChildren?.map((child) => ( ))}
@@ -309,13 +362,12 @@ export function AppShell() { {/* Navigation */} diff --git a/src/pages/admin/email-templates/[key].tsx b/src/pages/admin/email-templates/[key].tsx new file mode 100644 index 0000000..f340228 --- /dev/null +++ b/src/pages/admin/email-templates/[key].tsx @@ -0,0 +1,94 @@ +import { useParams, useNavigate } from "react-router-dom" +import { useQuery } from "@tanstack/react-query" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { ArrowLeft, ExternalLink, Loader2 } from "lucide-react" +import { emailService } from "@/services" + +export default function EmailTemplatePreviewPage() { + const { key } = useParams() + const navigate = useNavigate() + + const { data: templatesData } = useQuery({ + queryKey: ['admin', 'email-templates'], + queryFn: () => emailService.listPreviewTemplates(), + }) + + const { data: preview, isLoading, error } = useQuery({ + queryKey: ['admin', 'email-templates', key, 'preview'], + queryFn: () => emailService.getPreviewJson(key!), + enabled: !!key, + }) + + const templateMeta = templatesData?.templates.find((t) => t.key === key) + + if (isLoading) { + return ( +
+ + Rendering preview... +
+ ) + } + + if (error || !preview) { + return ( +
+ +
+ Failed to load email preview. +
+
+ ) + } + + return ( +
+
+
+ +
+

{templateMeta?.name ?? preview.templateKey}

+

{templateMeta?.description}

+
+
+ +
+ + + +
+ Subject + {preview.subject} + {preview.templateKey} +
+
+ +
+