@@ -22,7 +22,7 @@ import {
2222
2323import { ArrowLeft , RefreshCw , Trash2 , Clock , User , Mail , Phone , Link as LinkIcon } from "lucide-react" ;
2424import { format } from "date-fns" ;
25- import { Notification , NOTIFICATION_STATUS } from "@/lib/types" ;
25+ import { Notification , NOTIFICATION_STATUS , PluginMetadata , ProviderMetadata } from "@/lib/types" ;
2626import Link from "next/link" ;
2727import { useState } from "react" ;
2828import { toast } from "sonner" ;
@@ -45,8 +45,24 @@ export default function EventDetailPage({ params }: PageProps) {
4545 fetcher
4646 ) ;
4747
48+ // Fetch plugin metadata for dynamic schema rendering
49+ const { data : plugins } = useSWR < PluginMetadata > ( '/api/plugins' , fetcher ) ;
50+
4851 const [ retryDialogOpen , setRetryDialogOpen ] = useState ( false ) ;
4952
53+ // Helper to find provider metadata based on channel and provider ID
54+ const getProviderMetadata = (
55+ channel : string ,
56+ providerId : string | undefined
57+ ) : ProviderMetadata | undefined => {
58+ if ( ! plugins ?. channels [ channel ] ) return undefined ;
59+ const channelMeta = plugins . channels [ channel ] ;
60+ // Find by providerId, or use default, or use first provider
61+ return channelMeta . providers . find ( p => p . id === providerId )
62+ || channelMeta . providers . find ( p => p . id === channelMeta . default )
63+ || channelMeta . providers [ 0 ] ;
64+ } ;
65+
5066 const handleRetry = async ( ) => {
5167 if ( ! notification ) return ;
5268
@@ -289,25 +305,35 @@ export default function EventDetailPage({ params }: PageProps) {
289305 < CardDescription > Recipient details</ CardDescription >
290306 </ CardHeader >
291307 < CardContent className = "space-y-4" >
292- < div className = "flex items-center gap-2" >
293- < User className = "h-4 w-4 text-muted-foreground" />
294- < span className = "text-sm text-muted-foreground" > User ID:</ span >
295- < span className = "font-mono text-sm" > { String ( notification . recipient . user_id || '' ) } </ span >
296- </ div >
297- { Boolean ( notification . recipient . email ) && (
298- < div className = "flex items-center gap-2" >
299- < Mail className = "h-4 w-4 text-muted-foreground" />
300- < span className = "text-sm text-muted-foreground" > Email:</ span >
301- < span className = "text-sm" > { String ( notification . recipient . email ) } </ span >
302- </ div >
303- ) }
304- { Boolean ( notification . recipient . phone ) && (
305- < div className = "flex items-center gap-2" >
306- < Phone className = "h-4 w-4 text-muted-foreground" />
307- < span className = "text-sm text-muted-foreground" > Phone:</ span >
308- < span className = "text-sm" > { String ( notification . recipient . phone ) } </ span >
309- </ div >
310- ) }
308+ { ( ( ) => {
309+ const providerMeta = getProviderMetadata ( notification . channel , notification . provider ) ;
310+
311+ return (
312+ < >
313+ { providerMeta ?. recipientFields . map ( field => {
314+ const value = notification . recipient [ field . name ] ;
315+ if ( value === undefined || value === null || value === '' ) return null ;
316+
317+ // Choose icon based on field type
318+ const IconComponent = field . type === 'email' ? Mail
319+ : field . type === 'phone' ? Phone
320+ : User ;
321+
322+ return (
323+ < div key = { field . name } className = "flex items-center gap-2" >
324+ < IconComponent className = "h-4 w-4 text-muted-foreground" />
325+ < span className = "text-sm text-muted-foreground capitalize" >
326+ { field . name . replace ( / _ / g, ' ' ) } :
327+ </ span >
328+ < span className = { `text-sm ${ field . type === 'email' || field . name === 'user_id' ? 'font-mono' : '' } ` } >
329+ { String ( value ) }
330+ </ span >
331+ </ div >
332+ ) ;
333+ } ) }
334+ </ >
335+ ) ;
336+ } ) ( ) }
311337 < Separator />
312338 < div className = "flex items-start gap-2" >
313339 < LinkIcon className = "h-4 w-4 text-muted-foreground mt-0.5" />
@@ -325,41 +351,48 @@ export default function EventDetailPage({ params }: PageProps) {
325351 < CardTitle > Content</ CardTitle >
326352 < CardDescription > Message content for this notification</ CardDescription >
327353 </ CardHeader >
328- < CardContent >
329- { notification . channel === "email" && ( notification . content as Record < string , { subject ?: string ; message ?: string } > ) . email && (
330- < div className = "space-y-4" >
331- { ( notification . content as Record < string , { subject ?: string ; message ?: string } > ) . email ?. subject && (
332- < div >
333- < span className = "text-sm font-medium" > Subject:</ span >
334- < p className = "mt-1 text-sm" > { String ( ( notification . content as Record < string , { subject ?: string } > ) . email ?. subject ) } </ p >
354+ < CardContent className = "space-y-4" >
355+ { ( ( ) => {
356+ const providerMeta = getProviderMetadata ( notification . channel , notification . provider ) ;
357+ // Content may be nested under channel key or flat
358+ const channelContent = ( notification . content as Record < string , unknown > ) [ notification . channel ] as Record < string , unknown > | undefined
359+ || notification . content as Record < string , unknown > ;
360+
361+ // No schema available - fallback to JSON
362+ if ( ! providerMeta ?. contentFields ?. length ) {
363+ return (
364+ < pre className = "p-4 bg-muted rounded-lg text-xs overflow-auto" >
365+ { JSON . stringify ( notification . content , null , 2 ) }
366+ </ pre >
367+ ) ;
368+ }
369+
370+ // Render fields from schema
371+ return providerMeta . contentFields . map ( field => {
372+ const value = channelContent [ field . name ] ;
373+ if ( value === undefined || value === null || value === '' ) return null ;
374+
375+ const stringValue = String ( value ) ;
376+
377+ return (
378+ < div key = { field . name } >
379+ < span className = "text-sm font-medium capitalize" >
380+ { field . name . replace ( / _ / g, ' ' ) } :
381+ </ span >
382+ { field . type === 'text' ? (
383+ // Long text/HTML content - styled box
384+ < div
385+ className = "mt-2 p-4 bg-muted rounded-lg text-sm prose prose-sm max-w-none dark:prose-invert"
386+ dangerouslySetInnerHTML = { { __html : stringValue } }
387+ />
388+ ) : (
389+ // Short string content - inline
390+ < p className = "mt-1 text-sm" > { stringValue } </ p >
391+ ) }
335392 </ div >
336- ) }
337- < div >
338- < span className = "text-sm font-medium" > Message:</ span >
339- < div
340- className = "mt-2 p-4 bg-muted rounded-lg text-sm"
341- dangerouslySetInnerHTML = { { __html : String ( ( notification . content as Record < string , { message ?: string } > ) . email ?. message || '' ) } }
342- />
343- </ div >
344- </ div >
345- ) }
346- { notification . channel === "whatsapp" && ( notification . content as Record < string , { message ?: string } > ) . whatsapp && (
347- < div >
348- < span className = "text-sm font-medium" > Message:</ span >
349- < p className = "mt-2 p-4 bg-muted rounded-lg text-sm whitespace-pre-wrap" >
350- { String ( ( notification . content as Record < string , { message ?: string } > ) . whatsapp ?. message || '' ) }
351- </ p >
352- </ div >
353- ) }
354- { /* Generic content fallback for other channels */ }
355- { notification . channel !== "email" && notification . channel !== "whatsapp" && (
356- < div >
357- < span className = "text-sm font-medium" > Content:</ span >
358- < pre className = "mt-2 p-4 bg-muted rounded-lg text-xs overflow-auto" >
359- { JSON . stringify ( notification . content , null , 2 ) }
360- </ pre >
361- </ div >
362- ) }
393+ ) ;
394+ } ) ;
395+ } ) ( ) }
363396 </ CardContent >
364397 </ Card >
365398
0 commit comments