Files
sim/apps/sim/app/templates/[id]/template.tsx
Emir Karabeg 0083c89fa5 feat(ui): logs, kb, emcn (#2207)
* feat(kb): emcn alignment; sidebar: popover primary; settings-modal: expand

* feat: EMCN breadcrumb; improvement(KB): UI

* fix: hydration error

* improvement(KB): UI

* feat: emcn modal sizing, KB tags; refactor: deleted old sidebar

* feat(logs): UI

* fix: add documents modal name

* feat: logs, emcn, cursorrules; refactor: logs

* feat: dashboard

* feat: notifications; improvement: logs details

* fixed random rectangle on canvas

* fixed the name of the file to align

* fix build

---------

Co-authored-by: waleed <walif6@gmail.com>
2025-12-09 20:50:28 -08:00

1016 lines
38 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useState } from 'react'
import { formatDistanceToNow } from 'date-fns'
import {
ChartNoAxesColumn,
ChevronDown,
Globe,
Linkedin,
Mail,
Share2,
Star,
User,
} from 'lucide-react'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import ReactMarkdown from 'react-markdown'
import {
Breadcrumb,
Button,
Copy,
Popover,
PopoverContent,
PopoverItem,
PopoverTrigger,
} from '@/components/emcn'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { VerifiedBadge } from '@/components/ui/verified-badge'
import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
import type { CredentialRequirement } from '@/lib/workflows/credentials/credential-extractor'
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
import { getBlock } from '@/blocks/registry'
import { useStarTemplate, useTemplate } from '@/hooks/queries/templates'
const logger = createLogger('TemplateDetails')
interface TemplateDetailsProps {
isWorkspaceContext?: boolean
}
export default function TemplateDetails({ isWorkspaceContext = false }: TemplateDetailsProps) {
const router = useRouter()
const searchParams = useSearchParams()
const params = useParams()
const templateId = params?.id as string
const workspaceId = isWorkspaceContext ? (params?.workspaceId as string) : null
const { data: session } = useSession()
const { data: template, isLoading: loading } = useTemplate(templateId)
const starTemplate = useStarTemplate()
const [currentUserOrgs, setCurrentUserOrgs] = useState<string[]>([])
const [currentUserOrgRoles, setCurrentUserOrgRoles] = useState<
Array<{ organizationId: string; role: string }>
>([])
const [isSuperUser, setIsSuperUser] = useState(false)
const [isUsing, setIsUsing] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [isApproving, setIsApproving] = useState(false)
const [isRejecting, setIsRejecting] = useState(false)
const [isVerifying, setIsVerifying] = useState(false)
const [hasWorkspaceAccess, setHasWorkspaceAccess] = useState<boolean | null>(null)
const [workspaces, setWorkspaces] = useState<
Array<{ id: string; name: string; permissions: string }>
>([])
const [isLoadingWorkspaces, setIsLoadingWorkspaces] = useState(false)
const [showWorkspaceSelectorForEdit, setShowWorkspaceSelectorForEdit] = useState(false)
const [sharePopoverOpen, setSharePopoverOpen] = useState(false)
const currentUserId = session?.user?.id || null
useEffect(() => {
const fetchUserOrganizations = async () => {
if (!currentUserId) return
try {
const response = await fetch('/api/organizations')
if (response.ok) {
const data = await response.json()
const orgs = data.organizations || []
const orgIds = orgs.map((org: any) => org.id)
const orgRoles = orgs.map((org: any) => ({
organizationId: org.id,
role: org.role,
}))
setCurrentUserOrgs(orgIds)
setCurrentUserOrgRoles(orgRoles)
}
} catch (error) {
logger.error('Error fetching organizations:', error)
}
}
const fetchSuperUserStatus = async () => {
if (!currentUserId) return
try {
const response = await fetch('/api/user/super-user')
if (response.ok) {
const data = await response.json()
setIsSuperUser(data.isSuperUser || false)
}
} catch (error) {
logger.error('Error fetching super user status:', error)
}
}
fetchSuperUserStatus()
fetchUserOrganizations()
}, [currentUserId])
useEffect(() => {
if (!currentUserId) return
const fetchWorkspaces = async () => {
try {
setIsLoadingWorkspaces(true)
const response = await fetch('/api/workspaces')
if (response.ok) {
const data = await response.json()
const availableWorkspaces = data.workspaces
.filter((ws: any) => ws.permissions === 'write' || ws.permissions === 'admin')
.map((ws: any) => ({
id: ws.id,
name: ws.name,
permissions: ws.permissions,
}))
setWorkspaces(availableWorkspaces)
}
} catch (error) {
logger.error('Error fetching workspaces:', error)
} finally {
setIsLoadingWorkspaces(false)
}
}
fetchWorkspaces()
}, [currentUserId])
useEffect(() => {
if (template && searchParams?.get('use') === 'true' && currentUserId) {
if (isWorkspaceContext && workspaceId) {
handleWorkspaceSelectForUse(workspaceId)
router.replace(`/workspace/${workspaceId}/templates/${template.id}`)
} else {
router.replace(`/templates/${template.id}`)
}
}
}, [searchParams, currentUserId, template, isWorkspaceContext, workspaceId, router])
const canEditTemplate = (() => {
if (!currentUserId || !template?.creator) return false
if (template.creator.referenceType === 'user') {
return template.creator.referenceId === currentUserId
}
if (template.creator.referenceType === 'organization' && template.creator.referenceId) {
const isOrgMember = currentUserOrgs.includes(template.creator.referenceId)
if (template.workflowId) {
return isOrgMember
}
const orgMembership = currentUserOrgRoles.find(
(org) => org.organizationId === template.creator?.referenceId
)
const isAdminOrOwner = orgMembership?.role === 'admin' || orgMembership?.role === 'owner'
return isOrgMember && isAdminOrOwner
}
return false
})()
useEffect(() => {
const checkWorkspaceAccess = async () => {
if (!template?.workflowId || !currentUserId || !canEditTemplate) {
setHasWorkspaceAccess(null)
return
}
try {
const checkResponse = await fetch(`/api/workflows/${template.workflowId}`)
if (checkResponse.status === 403) {
setHasWorkspaceAccess(false)
} else if (checkResponse.ok) {
setHasWorkspaceAccess(true)
} else {
setHasWorkspaceAccess(null)
}
} catch (error) {
logger.error('Error checking workspace access:', error)
setHasWorkspaceAccess(null)
}
}
checkWorkspaceAccess()
}, [template?.workflowId, currentUserId, canEditTemplate])
if (loading) {
return (
<div className='flex h-screen items-center justify-center'>
<div className='text-center'>
<p className='font-sans text-muted-foreground text-sm'>Loading template...</p>
</div>
</div>
)
}
if (!template) {
return (
<div className='flex h-screen items-center justify-center'>
<div className='text-center'>
<h1 className='mb-4 font-sans font-semibold text-2xl'>Template Not Found</h1>
<p className='font-sans text-muted-foreground text-sm'>
The template you're looking for doesn't exist.
</p>
</div>
</div>
)
}
const renderWorkflowPreview = () => {
if (!template.state) {
return (
<div className='flex h-full items-center justify-center text-center'>
<div className='text-muted-foreground'>
<div className='mb-2 font-medium font-sans text-lg'> No Workflow Data</div>
<div className='font-sans text-sm'>
This template doesn't contain workflow state data.
</div>
</div>
</div>
)
}
try {
return (
<WorkflowPreview
workflowState={template.state}
showSubBlocks={true}
height='100%'
width='100%'
isPannable={true}
defaultPosition={{ x: 0, y: 0 }}
defaultZoom={1}
/>
)
} catch (error) {
logger.error('Error rendering workflow preview:', error)
return (
<div className='flex h-full items-center justify-center text-center'>
<div className='text-muted-foreground'>
<div className='mb-2 font-medium font-sans text-lg'>⚠️ Preview Error</div>
<div className='font-sans text-sm'>Unable to render workflow preview</div>
</div>
</div>
)
}
}
const breadcrumbItems = [
{
label: 'Templates',
href: isWorkspaceContext ? `/workspace/${workspaceId}/templates` : '/templates',
},
{ label: template?.name || 'Template' },
]
/**
* Intercepts wheel events over the workflow preview so that the page handles scrolling
* instead of the underlying canvas. We stop propagation in the capture phase to prevent
* React Flow from consuming the event, but intentionally avoid preventDefault so the
* browser can perform its normal scroll behavior.
*
* We allow zoom gestures (Ctrl/Cmd + wheel) to pass through unmodified.
*
* @param event - The wheel event fired when the user scrolls over the preview area.
*/
const handleCanvasWheelCapture = (event: React.WheelEvent<HTMLDivElement>) => {
if (event.ctrlKey || event.metaKey) {
return
}
event.stopPropagation()
}
const handleStarToggle = async () => {
if (!currentUserId || !template) return
starTemplate.mutate({
templateId: template.id,
action: template.isStarred ? 'remove' : 'add',
})
}
const handleUseTemplate = () => {
if (!currentUserId) {
const callbackUrl =
isWorkspaceContext && workspaceId
? encodeURIComponent(`/workspace/${workspaceId}/templates/${template.id}?use=true`)
: encodeURIComponent(`/templates/${template.id}`)
router.push(`/login?callbackUrl=${callbackUrl}`)
return
}
if (isWorkspaceContext && workspaceId) {
handleWorkspaceSelectForUse(workspaceId)
}
}
const handleEditTemplate = async () => {
if (!currentUserId || !template) return
if (isWorkspaceContext && workspaceId && template.workflowId) {
setIsEditing(true)
try {
const checkResponse = await fetch(`/api/workflows/${template.workflowId}`)
if (checkResponse.ok) {
router.push(`/workspace/${workspaceId}/w/${template.workflowId}`)
return
}
} catch (error) {
logger.error('Error checking workflow:', error)
} finally {
setIsEditing(false)
}
}
if (template.workflowId && !isWorkspaceContext) {
setIsEditing(true)
try {
const checkResponse = await fetch(`/api/workflows/${template.workflowId}`)
if (checkResponse.status === 403) {
alert("You don't have access to the workspace containing this template")
return
}
if (checkResponse.ok) {
const result = await checkResponse.json()
const templateWorkspaceId = result.data?.workspaceId
if (templateWorkspaceId) {
window.location.href = `/workspace/${templateWorkspaceId}/w/${template.workflowId}`
return
}
}
} catch (error) {
logger.error('Error checking workflow:', error)
} finally {
setIsEditing(false)
}
}
if (isWorkspaceContext && workspaceId) {
handleWorkspaceSelectForEdit(workspaceId)
} else {
setShowWorkspaceSelectorForEdit(true)
}
}
const handleWorkspaceSelectForUse = async (workspaceId: string) => {
if (isUsing || !template) return
setIsUsing(true)
try {
const response = await fetch(`/api/templates/${template.id}/use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workspaceId }),
})
if (!response.ok) {
throw new Error('Failed to use template')
}
const { workflowId } = await response.json()
window.location.href = `/workspace/${workspaceId}/w/${workflowId}`
} catch (error) {
logger.error('Error using template:', error)
} finally {
setIsUsing(false)
}
}
const handleWorkspaceSelectForEdit = async (workspaceId: string) => {
if (isUsing || !template) return
setIsUsing(true)
setShowWorkspaceSelectorForEdit(false)
try {
const response = await fetch(`/api/templates/${template.id}/use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workspaceId, connectToTemplate: true }),
})
if (!response.ok) {
throw new Error('Failed to import template for editing')
}
const { workflowId } = await response.json()
window.location.href = `/workspace/${workspaceId}/w/${workflowId}`
} catch (error) {
logger.error('Error importing template for editing:', error)
} finally {
setIsUsing(false)
}
}
const handleApprove = async () => {
if (isApproving || !template) return
setIsApproving(true)
try {
const response = await fetch(`/api/templates/${template.id}/approve`, {
method: 'POST',
})
if (response.ok) {
if (isWorkspaceContext && workspaceId) {
router.push(`/workspace/${workspaceId}/templates`)
} else {
router.push('/templates')
}
}
} catch (error) {
logger.error('Error approving template:', error)
} finally {
setIsApproving(false)
}
}
const handleReject = async () => {
if (isRejecting || !template) return
setIsRejecting(true)
try {
const response = await fetch(`/api/templates/${template.id}/reject`, {
method: 'POST',
})
if (response.ok) {
if (isWorkspaceContext && workspaceId) {
router.push(`/workspace/${workspaceId}/templates`)
} else {
router.push('/templates')
}
}
} catch (error) {
logger.error('Error rejecting template:', error)
} finally {
setIsRejecting(false)
}
}
const handleToggleVerification = async () => {
if (isVerifying || !template?.creator?.id) return
setIsVerifying(true)
try {
const endpoint = `/api/creators/${template.creator.id}/verify`
const method = template.creator.verified ? 'DELETE' : 'POST'
const response = await fetch(endpoint, { method })
if (response.ok) {
// Refresh page to show updated verification status
window.location.reload()
} else {
const error = await response.json()
logger.error('Error toggling verification:', error)
alert(`Failed to ${template.creator.verified ? 'unverify' : 'verify'} creator`)
}
} catch (error) {
logger.error('Error toggling verification:', error)
alert('An error occurred while toggling verification')
} finally {
setIsVerifying(false)
}
}
/**
* Shares the template to X (Twitter)
*/
const handleShareToTwitter = () => {
if (!template) return
setSharePopoverOpen(false)
const templateUrl = `${getBaseUrl()}/templates/${template.id}`
let tweetText = `🚀 Check out this workflow template: ${template.name}`
if (template.details?.tagline) {
const taglinePreview =
template.details.tagline.length > 100
? `${template.details.tagline.substring(0, 100)}...`
: template.details.tagline
tweetText += `\n\n${taglinePreview}`
}
const maxTextLength = 280 - 23 - 1
if (tweetText.length > maxTextLength) {
tweetText = `${tweetText.substring(0, maxTextLength - 3)}...`
}
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(templateUrl)}`
window.open(twitterUrl, '_blank', 'noopener,noreferrer')
}
/**
* Shares the template to LinkedIn.
*/
const handleShareToLinkedIn = () => {
if (!template) return
setSharePopoverOpen(false)
const templateUrl = `${getBaseUrl()}/templates/${template.id}`
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(templateUrl)}`
window.open(linkedInUrl, '_blank', 'noopener,noreferrer')
}
const handleCopyLink = async () => {
setSharePopoverOpen(false)
const templateUrl = `${getBaseUrl()}/templates/${template?.id}`
try {
await navigator.clipboard.writeText(templateUrl)
logger.info('Template link copied to clipboard')
} catch (error) {
logger.error('Failed to copy link:', error)
}
}
return (
<div className={cn('flex flex-col', isWorkspaceContext ? 'h-full flex-1' : 'min-h-screen')}>
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-auto px-[24px] pt-[24px] pb-[24px]'>
{/* Breadcrumb navigation */}
<Breadcrumb items={breadcrumbItems} />
{/* Template name and action buttons */}
<div className='mt-[14px] flex items-center justify-between'>
<h1 className='font-medium text-[18px] text-[var(--text-primary)]'>{template.name}</h1>
{/* Action buttons */}
<div className='flex items-center gap-[8px]'>
{/* Approve/Reject buttons for super users */}
{isSuperUser && template.status === 'pending' && (
<>
<Button
variant='active'
onClick={handleApprove}
disabled={isApproving}
className='h-[32px] rounded-[6px]'
>
{isApproving ? 'Approving...' : 'Approve'}
</Button>
<Button
variant='active'
onClick={handleReject}
disabled={isRejecting}
className='h-[32px] rounded-[6px]'
>
{isRejecting ? 'Rejecting...' : 'Reject'}
</Button>
</>
)}
{/* Edit button - for template owners */}
{canEditTemplate && currentUserId && (
<>
{(isWorkspaceContext || template.workflowId) && !showWorkspaceSelectorForEdit ? (
<Button
variant='active'
onClick={handleEditTemplate}
disabled={isEditing || (!isWorkspaceContext && hasWorkspaceAccess === false)}
className='h-[32px] rounded-[6px]'
>
{isEditing ? 'Opening...' : 'Edit'}
</Button>
) : (
<DropdownMenu
open={showWorkspaceSelectorForEdit}
onOpenChange={setShowWorkspaceSelectorForEdit}
>
<DropdownMenuTrigger asChild>
<Button
variant='active'
onClick={() => setShowWorkspaceSelectorForEdit(true)}
disabled={isUsing || isLoadingWorkspaces}
className='h-[32px] rounded-[6px]'
>
{isUsing ? 'Importing...' : isLoadingWorkspaces ? 'Loading...' : 'Edit'}
<ChevronDown className='ml-2 h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-56'>
{workspaces.length === 0 ? (
<DropdownMenuItem disabled className='text-muted-foreground text-sm'>
No workspaces with write access
</DropdownMenuItem>
) : (
workspaces.map((workspace) => (
<DropdownMenuItem
key={workspace.id}
onClick={() => handleWorkspaceSelectForEdit(workspace.id)}
className='flex cursor-pointer items-center justify-between'
>
<div className='flex flex-col'>
<span className='font-medium text-sm'>{workspace.name}</span>
<span className='text-muted-foreground text-xs capitalize'>
{workspace.permissions} access
</span>
</div>
</DropdownMenuItem>
))
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</>
)}
{/* Use template button - only for approved templates and non-owners */}
{template.status === 'approved' && !canEditTemplate && (
<>
{!currentUserId ? (
<Button
variant='primary'
onClick={() => {
const callbackUrl =
isWorkspaceContext && workspaceId
? encodeURIComponent(
`/workspace/${workspaceId}/templates/${template.id}?use=true`
)
: encodeURIComponent(`/templates/${template.id}`)
router.push(`/login?callbackUrl=${callbackUrl}`)
}}
className='h-[32px] rounded-[6px]'
>
Sign in to use
</Button>
) : isWorkspaceContext ? (
<Button
variant='primary'
onClick={handleUseTemplate}
disabled={isUsing}
className='!text-[#FFFFFF] h-[32px] rounded-[6px] px-[12px] text-[14px]'
>
{isUsing ? 'Creating...' : 'Use template'}
</Button>
) : null}
</>
)}
{/* Share button */}
<Popover open={sharePopoverOpen} onOpenChange={setSharePopoverOpen}>
<PopoverTrigger asChild>
<Button variant='active' className='h-[32px] rounded-[6px] px-[12px]'>
<Share2 className='h-[14px] w-[14px]' />
</Button>
</PopoverTrigger>
<PopoverContent align='end' side='bottom' sideOffset={8}>
<PopoverItem onClick={handleCopyLink}>
<Copy className='h-3 w-3' />
<span>Copy link</span>
</PopoverItem>
<PopoverItem onClick={handleShareToTwitter}>
<svg
className='h-3 w-3'
viewBox='0 0 24 24'
fill='currentColor'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' />
</svg>
<span>Share on X</span>
</PopoverItem>
<PopoverItem onClick={handleShareToLinkedIn}>
<Linkedin className='h-3 w-3' />
<span>Share on LinkedIn</span>
</PopoverItem>
</PopoverContent>
</Popover>
</div>
</div>
{/* Template tagline */}
{template.details?.tagline && (
<p className='mt-[4px] font-medium text-[14px] text-[var(--text-tertiary)]'>
{template.details.tagline}
</p>
)}
{/* Creator and stats row */}
<div className='mt-[16px] flex items-center gap-[8px]'>
{/* Star icon and count */}
<Star
onClick={handleStarToggle}
className={cn(
'h-[14px] w-[14px] cursor-pointer transition-colors',
template.isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[var(--text-muted)]',
starTemplate.isPending && 'opacity-50'
)}
/>
<span className='font-medium text-[14px] text-[var(--text-muted)]'>
{template.stars || 0}
</span>
{/* Users icon and count */}
<ChartNoAxesColumn className='h-[16px] w-[16px] text-[var(--text-muted)]' />
<span className='font-medium text-[14px] text-[var(--text-muted)]'>
{template.views}
</span>
{/* Vertical divider */}
<div className='mx-[4px] mb-[-1.5px] h-[18px] w-[1.25px] rounded-full bg-[var(--border)]' />
{/* Creator profile pic */}
{template.creator?.profileImageUrl ? (
<div className='h-[16px] w-[16px] flex-shrink-0 overflow-hidden rounded-full'>
<img
src={template.creator.profileImageUrl}
alt={template.creator.name}
className='h-full w-full object-cover'
/>
</div>
) : (
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-elevated)]'>
<User className='h-[14px] w-[14px] text-[var(--text-muted)]' />
</div>
)}
{/* Creator name */}
<div className='flex items-center gap-[4px]'>
<span className='font-medium text-[14px] text-[var(--text-muted)]'>
{template.creator?.name || 'Unknown'}
</span>
{template.creator?.verified && <VerifiedBadge size='md' />}
</div>
</div>
{/* Credentials needed */}
{Array.isArray(template.requiredCredentials) &&
template.requiredCredentials.length > 0 && (
<p className='mt-[12px] font-medium text-[12px] text-[var(--text-muted)]'>
Credentials needed:{' '}
{template.requiredCredentials
.map((cred: CredentialRequirement) => {
const blockName =
getBlock(cred.blockType)?.name ||
cred.blockType.charAt(0).toUpperCase() + cred.blockType.slice(1)
const alreadyHasBlock = cred.label
.toLowerCase()
.includes(` for ${blockName.toLowerCase()}`)
return alreadyHasBlock ? cred.label : `${cred.label} for ${blockName}`
})
.join(', ')}
</p>
)}
{/* Canvas preview */}
<div
className='relative mt-[24px] h-[450px] w-full overflow-hidden rounded-[8px] border border-[var(--border)]'
onWheelCapture={handleCanvasWheelCapture}
>
{renderWorkflowPreview()}
{/* Last updated overlay */}
{template.updatedAt && (
<div className='pointer-events-none absolute right-[12px] bottom-[12px] rounded-[4px] bg-[var(--bg)]/80 px-[8px] py-[4px] backdrop-blur-sm'>
<span className='font-medium text-[12px] text-[var(--text-muted)]'>
Last updated{' '}
{formatDistanceToNow(new Date(template.updatedAt), {
addSuffix: true,
})}
</span>
</div>
)}
</div>
{/* About this Workflow */}
{template.details?.about && (
<div className='mt-8'>
<h3 className='mb-4 font-sans font-semibold text-base text-foreground'>
About this Workflow
</h3>
<div className='max-w-none space-y-2'>
<ReactMarkdown
components={{
p: ({ children }) => (
<p className='mb-2 font-sans text-muted-foreground text-sm leading-[1.4rem] last:mb-0'>
{children}
</p>
),
h1: ({ children }) => (
<h1 className='mt-6 mb-3 font-sans font-semibold text-foreground text-xl first:mt-0'>
{children}
</h1>
),
h2: ({ children }) => (
<h2 className='mt-5 mb-2.5 font-sans font-semibold text-foreground text-lg first:mt-0'>
{children}
</h2>
),
h3: ({ children }) => (
<h3 className='mt-4 mb-2 font-sans font-semibold text-base text-foreground first:mt-0'>
{children}
</h3>
),
h4: ({ children }) => (
<h4 className='mt-3 mb-2 font-sans font-semibold text-foreground text-sm first:mt-0'>
{children}
</h4>
),
ul: ({ children }) => (
<ul className='my-2 ml-5 list-disc space-y-1.5 font-sans text-muted-foreground text-sm'>
{children}
</ul>
),
ol: ({ children }) => (
<ol className='my-2 ml-5 list-decimal space-y-1.5 font-sans text-muted-foreground text-sm'>
{children}
</ol>
),
li: ({ children }) => <li className='leading-[1.4rem]'>{children}</li>,
code: ({ inline, children }: any) =>
inline ? (
<code className='rounded bg-muted px-1.5 py-0.5 font-mono text-[#F59E0B] text-xs'>
{children}
</code>
) : (
<code className='my-2 block overflow-x-auto rounded-md bg-muted p-3 font-mono text-foreground text-xs'>
{children}
</code>
),
a: ({ href, children }) => (
<a
href={href}
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 underline-offset-2 transition-colors hover:text-blue-500 hover:underline dark:text-blue-400 dark:hover:text-blue-300'
>
{children}
</a>
),
strong: ({ children }) => (
<strong className='font-sans font-semibold text-foreground'>
{children}
</strong>
),
em: ({ children }) => <em className='text-muted-foreground'>{children}</em>,
}}
>
{template.details.about}
</ReactMarkdown>
</div>
</div>
)}
{/* About the Creator */}
{template.creator &&
(template.creator.details?.about ||
template.creator.details?.xUrl ||
template.creator.details?.linkedinUrl ||
template.creator.details?.websiteUrl ||
template.creator.details?.contactEmail) && (
<div className='mt-8'>
<div className='mb-4 flex items-center justify-between'>
<h3 className='font-sans font-semibold text-base text-foreground'>
About the Creator
</h3>
{isSuperUser && template.creator && (
<Button
variant={template.creator.verified ? 'active' : 'default'}
onClick={handleToggleVerification}
disabled={isVerifying}
className='h-[28px] rounded-[6px] text-[12px]'
>
{isVerifying
? 'Updating...'
: template.creator.verified
? 'Unverify Creator'
: 'Verify Creator'}
</Button>
)}
</div>
<div className='flex items-start gap-4'>
{/* Creator profile image */}
{template.creator.profileImageUrl ? (
<div className='h-[48px] w-[48px] flex-shrink-0 overflow-hidden rounded-full'>
<img
src={template.creator.profileImageUrl}
alt={template.creator.name}
className='h-full w-full object-cover'
/>
</div>
) : (
<div className='flex h-[48px] w-[48px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-elevated)]'>
<User className='h-[24px] w-[24px] text-[var(--text-muted)]' />
</div>
)}
{/* Creator details */}
<div className='flex-1'>
<div className='mb-[5px] flex items-center gap-3'>
<div className='flex items-center gap-[6px]'>
<h4 className='font-sans font-semibold text-base text-foreground'>
{template.creator.name}
</h4>
{template.creator.verified && <VerifiedBadge size='md' />}
</div>
{/* Social links */}
<div className='flex items-center gap-[12px]'>
{template.creator.details?.websiteUrl && (
<a
href={template.creator.details.websiteUrl}
target='_blank'
rel='noopener noreferrer'
className='flex items-center text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
aria-label='Website'
>
<Globe className='h-[14px] w-[14px]' />
</a>
)}
{template.creator.details?.xUrl && (
<a
href={template.creator.details.xUrl}
target='_blank'
rel='noopener noreferrer'
className='flex items-center text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
aria-label='X (Twitter)'
>
<svg
className='h-[14px] w-[14px]'
viewBox='0 0 24 24'
fill='currentColor'
>
<path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' />
</svg>
</a>
)}
{template.creator.details?.linkedinUrl && (
<a
href={template.creator.details.linkedinUrl}
target='_blank'
rel='noopener noreferrer'
className='flex items-center text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
aria-label='LinkedIn'
>
<Linkedin className='h-[14px] w-[14px]' />
</a>
)}
{template.creator.details?.contactEmail && (
<a
href={`mailto:${template.creator.details.contactEmail}`}
className='flex items-center text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
aria-label='Email'
>
<Mail className='h-[14px] w-[14px]' />
</a>
)}
</div>
</div>
{/* Creator bio */}
{template.creator.details?.about && (
<div className='max-w-none'>
<ReactMarkdown
components={{
p: ({ children }) => (
<p className='mb-2 font-sans text-muted-foreground text-sm leading-[1.4rem] last:mb-0'>
{children}
</p>
),
a: ({ href, children }) => (
<a
href={href}
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 underline-offset-2 transition-colors hover:text-blue-500 hover:underline dark:text-blue-400 dark:hover:text-blue-300'
>
{children}
</a>
),
strong: ({ children }) => (
<strong className='font-sans font-semibold text-foreground'>
{children}
</strong>
),
}}
>
{template.creator.details.about}
</ReactMarkdown>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
</div>
)
}