fix(templates): fix template details page (#1940)

* Fix v1

* Template details page
This commit is contained in:
Siddharth Ganesan
2025-11-12 10:45:34 -08:00
committed by GitHub
parent e1a46c90c6
commit cb39e697e2
4 changed files with 383 additions and 307 deletions
@@ -1,105 +1,9 @@
import { db } from '@sim/db'
import { templateCreators, templateStars, templates } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { notFound } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import TemplateDetails from '@/app/workspace/[workspaceId]/templates/[id]/template'
const logger = createLogger('TemplatePage')
interface TemplatePageProps {
params: Promise<{
workspaceId: string
id: string
}>
}
export default async function TemplatePage({ params }: TemplatePageProps) {
const { workspaceId, id } = await params
try {
if (!id || typeof id !== 'string' || id.length !== 36) {
notFound()
}
const session = await getSession()
const templateData = await db
.select({
template: templates,
creator: templateCreators,
})
.from(templates)
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
.where(eq(templates.id, id))
.limit(1)
if (templateData.length === 0) {
notFound()
}
const { template, creator } = templateData[0]
if (!session?.user?.id && template.status !== 'approved') {
notFound()
}
if (!template.id || !template.name) {
logger.error('Template missing required fields:', {
id: template.id,
name: template.name,
})
notFound()
}
let isStarred = false
if (session?.user?.id) {
try {
const starData = await db
.select({ id: templateStars.id })
.from(templateStars)
.where(
and(
eq(templateStars.templateId, template.id),
eq(templateStars.userId, session.user.id)
)
)
.limit(1)
isStarred = starData.length > 0
} catch {
isStarred = false
}
}
const serializedTemplate = {
...template,
creator: creator || null,
createdAt: template.createdAt.toISOString(),
updatedAt: template.updatedAt.toISOString(),
isStarred,
}
return (
<TemplateDetails
template={JSON.parse(JSON.stringify(serializedTemplate))}
workspaceId={workspaceId}
currentUserId={session?.user?.id || null}
/>
)
} catch (error) {
logger.error('Error loading template:', error)
return (
<div className='flex h-[100vh] items-center justify-center pl-64'>
<div className='text-center'>
<h1 className='mb-[14px] font-medium text-[18px]'>Error Loading Template</h1>
<p className='text-[#888888] text-[14px]'>There was an error loading this template.</p>
<p className='mt-[10px] text-[#888888] text-[12px]'>Template ID: {id}</p>
<p className='mt-[10px] text-[12px] text-red-500'>
{error instanceof Error ? error.message : 'Unknown error'}
</p>
</div>
</div>
)
}
/**
* Workspace-scoped template detail page.
* Data fetching is handled client-side in the TemplateDetails component.
*/
export default function TemplatePage() {
return <TemplateDetails />
}
@@ -26,6 +26,7 @@ import {
Layers,
Lightbulb,
LineChart,
Linkedin,
Mail,
Megaphone,
MessageSquare,
@@ -39,28 +40,26 @@ import {
Star,
Target,
TrendingUp,
Twitter,
User,
Users,
Workflow,
Wrench,
Zap,
} from 'lucide-react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import ReactMarkdown from 'react-markdown'
import { Button } from '@/components/emcn'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import type { CredentialRequirement } from '@/lib/workflows/credential-extractor'
import type { Template } from '@/app/workspace/[workspaceId]/templates/templates'
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
import { getBlock } from '@/blocks/registry'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('TemplateDetails')
interface TemplateDetailsProps {
template: Template
workspaceId: string
currentUserId: string | null
}
// Icon mapping - reuse from template-card
const iconMap = {
FileText,
@@ -115,22 +114,69 @@ const getIconComponent = (icon: string): React.ReactNode => {
)
}
export default function TemplateDetails({
template,
workspaceId,
currentUserId,
}: TemplateDetailsProps) {
/**
* Template detail page component
* Fetches and displays detailed information about a specific template
*/
export default function TemplateDetails() {
const router = useRouter()
const searchParams = useSearchParams()
const params = useParams()
// Initialize all state hooks first (hooks must be called unconditionally)
const [isStarred, setIsStarred] = useState(template?.isStarred || false)
const [starCount, setStarCount] = useState(template?.stars || 0)
const workspaceId = params?.workspaceId as string
const templateId = params?.id as string
// State for template data
const [template, setTemplate] = useState<Template | null>(null)
const [currentUserId, setCurrentUserId] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [isStarred, setIsStarred] = useState(false)
const [starCount, setStarCount] = useState(0)
const [isStarring, setIsStarring] = useState(false)
const [isUsing, setIsUsing] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const isOwner = currentUserId && template?.userId === currentUserId
// Fetch template data on client side
useEffect(() => {
if (!templateId || !workspaceId) {
setLoading(false)
return
}
const fetchTemplate = async () => {
try {
const response = await fetch(`/api/templates/${templateId}`)
if (response.ok) {
const data = await response.json()
setTemplate(data.data)
setIsStarred(data.data.isStarred || false)
setStarCount(data.data.stars || 0)
}
} catch (error) {
logger.error('Error fetching template:', error)
} finally {
setLoading(false)
}
}
const fetchCurrentUser = async () => {
try {
const response = await fetch('/api/auth/get-session')
if (response.ok) {
const data = await response.json()
setCurrentUserId(data?.user?.id || null)
} else {
setCurrentUserId(null)
}
} catch (error) {
logger.error('Error fetching session:', error)
setCurrentUserId(null)
}
}
fetchTemplate()
fetchCurrentUser()
}, [templateId, workspaceId])
// Auto-use template after login if use=true query param is present
useEffect(() => {
@@ -138,47 +184,50 @@ export default function TemplateDetails({
const shouldAutoUse = searchParams?.get('use') === 'true'
if (shouldAutoUse && currentUserId && !isUsing) {
handleUseTemplate()
// Clean up URL
router.replace(`/workspace/${workspaceId}/templates/${template.id}`)
}
}, [searchParams, currentUserId, template?.id])
// Defensive check for template AFTER initializing hooks
if (!template) {
logger.error('Template prop is undefined or null in TemplateDetails component', {
template,
workspaceId,
currentUserId,
})
if (loading) {
return (
<div className='flex h-[100vh] items-center justify-center pl-64'>
<div className='text-center'>
<h1 className='mb-[14px] font-medium text-[18px]'>Template Not Found</h1>
<p className='text-[#888888] text-[14px]'>
The template you're looking for doesn't exist.
</p>
<p className='mt-[10px] text-[#888888] text-[12px]'>Template data failed to load</p>
<div className='mb-[14px] font-medium text-[18px]'>Loading...</div>
<p className='text-[14px] text-[var(--text-tertiary)]'>Fetching template details</p>
</div>
</div>
)
}
logger.info('Template loaded in TemplateDetails', {
id: template.id,
name: template.name,
hasState: !!template.state,
})
if (!template) {
return (
<div className='flex h-[100vh] items-center justify-center pl-64'>
<div className='text-center'>
<h1 className='mb-[14px] font-medium text-[18px]'>Template Not Found</h1>
<p className='text-[14px] text-[var(--text-tertiary)]'>
The template you're looking for doesn't exist.
</p>
</div>
</div>
)
}
const templateAuthor = template.author || template.creator?.name || 'Unknown'
const templateAuthorType = template.authorType || template.creator?.referenceType || 'user'
const templateDescription = template.description || template.details?.tagline || null
const templateColor = template.color || 'var(--brand-primary)'
const templateIcon = template.icon || 'Workflow'
const templateOwnerId =
template.userId ||
(template.creator?.referenceType === 'user' ? template.creator.referenceId : null)
const isOwner = currentUserId && templateOwnerId === currentUserId
/**
* Render workflow preview with consistent error handling
*/
const renderWorkflowPreview = () => {
// Follow the same pattern as deployed-workflow-card.tsx
if (!template?.state) {
logger.info('Template has no state:', template)
return (
<div className='flex h-full items-center justify-center text-center'>
<div className='text-[#888888]'>
<div className='text-[var(--text-tertiary)]'>
<div className='mb-[10px] font-medium text-[14px]'>⚠️ No Workflow Data</div>
<div className='text-[12px]'>This template doesn't contain workflow state data.</div>
</div>
@@ -186,11 +235,6 @@ export default function TemplateDetails({
)
}
logger.info('Template state:', template.state)
logger.info('Template state type:', typeof template.state)
logger.info('Template state blocks:', template.state.blocks)
logger.info('Template state edges:', template.state.edges)
try {
return (
<WorkflowPreview
@@ -207,7 +251,7 @@ export default function TemplateDetails({
logger.error('Error rendering workflow preview:', error)
return (
<div className='flex h-full items-center justify-center text-center'>
<div className='text-[#888888]'>
<div className='text-[var(--text-tertiary)]'>
<div className='mb-[10px] font-medium text-[14px]'>⚠️ Preview Error</div>
<div className='text-[12px]'>Unable to render workflow preview</div>
</div>
@@ -242,9 +286,7 @@ export default function TemplateDetails({
const handleUseTemplate = async () => {
if (isUsing) return
// Check if user is logged in
if (!currentUserId) {
// Redirect to login with callback URL to use template after login
const callbackUrl = encodeURIComponent(
`/workspace/${workspaceId}/templates/${template.id}?use=true`
)
@@ -265,8 +307,6 @@ export default function TemplateDetails({
}
const { workflowId } = await response.json()
// Navigate to the new workflow
router.push(`/workspace/${workspaceId}/w/${workflowId}`)
} catch (error) {
logger.error('Error using template:', error)
@@ -280,19 +320,15 @@ export default function TemplateDetails({
setIsEditing(true)
try {
// If template already has a connected workflowId, check if it exists in user's workspace
if (template.workflowId) {
// Try to fetch the workflow to see if it still exists
const checkResponse = await fetch(`/api/workflows/${template.workflowId}`)
if (checkResponse.ok) {
// Workflow exists, redirect to it
router.push(`/workspace/${workspaceId}/w/${template.workflowId}`)
return
}
}
// No connected workflow or it was deleted - create a new one
const response = await fetch(`/api/templates/${template.id}/edit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -304,8 +340,6 @@ export default function TemplateDetails({
}
const { workflowId } = await response.json()
// Navigate to the workflow
router.push(`/workspace/${workspaceId}/w/${workflowId}`)
} catch (error) {
logger.error('Error editing template:', error)
@@ -317,34 +351,31 @@ export default function TemplateDetails({
return (
<div className='flex h-[100vh] flex-col pl-64'>
<div className='flex flex-1 flex-col overflow-auto px-[24px] pt-[24px] pb-[24px]'>
{/* Back button */}
<button
onClick={handleBack}
className='mb-[14px] flex items-center gap-[8px] text-[#888888] transition-colors hover:text-white'
className='mb-[14px] flex items-center gap-[8px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
>
<ArrowLeft className='h-[14px] w-[14px]' />
<span className='font-medium text-[12px]'>Go back</span>
</button>
{/* Header */}
<div>
<div className='flex items-start gap-[12px]'>
{/* Icon */}
<div
className='flex h-[26px] w-[26px] items-center justify-center rounded-[6px]'
style={{ backgroundColor: template.color }}
style={{ backgroundColor: templateColor }}
>
{getIconComponent(template.icon)}
{getIconComponent(templateIcon)}
</div>
<h1 className='font-medium text-[18px]'>{template.name}</h1>
</div>
<p className='mt-[10px] font-base text-[#888888] text-[14px]'>{template.description}</p>
<p className='mt-[10px] font-base text-[14px] text-[var(--text-tertiary)]'>
{templateDescription}
</p>
</div>
{/* Stats and Actions */}
<div className='mt-[14px] flex items-center justify-between'>
{/* Stats */}
<div className='flex items-center gap-[12px] font-medium text-[#888888] text-[12px]'>
<div className='flex items-center gap-[12px] font-medium text-[12px] text-[var(--text-tertiary)]'>
<div className='flex items-center gap-[6px]'>
<Eye className='h-[12px] w-[12px]' />
<span>{template.views} views</span>
@@ -355,9 +386,9 @@ export default function TemplateDetails({
</div>
<div className='flex items-center gap-[6px]'>
<User className='h-[12px] w-[12px]' />
<span>by {template.author}</span>
<span>by {templateAuthor}</span>
</div>
{template.authorType === 'organization' && (
{templateAuthorType === 'organization' && (
<div className='flex items-center gap-[6px]'>
<Users className='h-[12px] w-[12px]' />
<span>Organization</span>
@@ -407,16 +438,128 @@ export default function TemplateDetails({
</div>
</div>
{/* Divider */}
<div className='mt-[24px] h-[1px] w-full border-[var(--border)] border-t' />
{/* Workflow preview */}
<div className='mt-[24px] flex-1'>
{/* Creator Profile */}
{template.creator && (
<div className='mt-[24px]'>
<h3 className='mb-[12px] font-medium text-[14px]'>Creator</h3>
<div className='rounded-[8px] border border-[var(--border)] bg-[var(--surface-3)] p-[16px]'>
<div className='flex items-start gap-[16px]'>
<div className='flex-shrink-0'>
{template.creator.profileImageUrl ? (
<div className='relative h-[56px] w-[56px] 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-[56px] w-[56px] items-center justify-center rounded-full bg-[var(--brand-primary)]'>
<User className='h-[28px] w-[28px] text-white' />
</div>
)}
</div>
<div className='flex-1'>
<h4 className='font-medium text-[14px]'>{template.creator.name}</h4>
{template.creator.details?.about && (
<p className='mt-[8px] text-[13px] text-[var(--text-tertiary)] leading-relaxed'>
{template.creator.details.about}
</p>
)}
{(template.creator.details?.xUrl ||
template.creator.details?.linkedinUrl ||
template.creator.details?.websiteUrl ||
template.creator.details?.contactEmail) && (
<div className='mt-[12px] flex flex-wrap gap-[12px]'>
{template.creator.details.xUrl && (
<a
href={template.creator.details.xUrl}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center gap-[6px] text-[13px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
>
<Twitter className='h-[14px] w-[14px]' />
<span>X</span>
</a>
)}
{template.creator.details.linkedinUrl && (
<a
href={template.creator.details.linkedinUrl}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center gap-[6px] text-[13px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
>
<Linkedin className='h-[14px] w-[14px]' />
<span>LinkedIn</span>
</a>
)}
{template.creator.details.websiteUrl && (
<a
href={template.creator.details.websiteUrl}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center gap-[6px] text-[13px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
>
<Globe className='h-[14px] w-[14px]' />
<span>Website</span>
</a>
)}
{template.creator.details.contactEmail && (
<a
href={`mailto:${template.creator.details.contactEmail}`}
className='inline-flex items-center gap-[6px] text-[13px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
>
<Mail className='h-[14px] w-[14px]' />
<span>Contact</span>
</a>
)}
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* Description */}
{template.details?.about && (
<div className='mt-[24px]'>
<h3 className='mb-[12px] font-medium text-[14px]'>Description</h3>
<div className='prose prose-sm dark:prose-invert max-w-none text-[13px] text-[var(--text-secondary)]'>
<ReactMarkdown>{template.details.about}</ReactMarkdown>
</div>
</div>
)}
<div className='mt-[24px]'>
<h2 className='mb-[14px] font-medium text-[14px]'>Workflow Preview</h2>
<div className='h-[calc(100vh-280px)] w-full overflow-hidden rounded-[8px] bg-[#202020]'>
<div className='h-[600px] w-full overflow-hidden rounded-[8px] bg-[var(--surface-3)]'>
{renderWorkflowPreview()}
</div>
</div>
{/* Required Credentials */}
{Array.isArray(template.requiredCredentials) && template.requiredCredentials.length > 0 && (
<div className='mt-[24px]'>
<h3 className='mb-[12px] font-medium text-[14px]'>Credentials Needed</h3>
<ul className='list-disc space-y-[4px] pl-[20px] text-[13px] text-[var(--text-tertiary)]'>
{template.requiredCredentials.map((cred: CredentialRequirement, idx: number) => {
const blockName =
getBlock(cred.blockType)?.name ||
cred.blockType.charAt(0).toUpperCase() + cred.blockType.slice(1)
const alreadyHasBlock = cred.label
.toLowerCase()
.includes(` for ${blockName.toLowerCase()}`)
const text = alreadyHasBlock ? cred.label : `${cred.label} for ${blockName}`
return <li key={idx}>{text}</li>
})}
</ul>
</div>
)}
</div>
</div>
)
@@ -34,13 +34,13 @@ export default async function TemplatesPage() {
effectiveSuperUser = isSuperUser && superUserModeEnabled
}
// Load templates (same logic as global page)
// Load templates from database
let rows:
| Array<{
id: string
workflowId: string | null
name: string
details?: any
details?: unknown
creatorId: string | null
creator: {
id: string
@@ -124,24 +124,46 @@ export default async function TemplatesPage() {
row.creator?.referenceType === 'user' ? row.creator.referenceId : '' /* no owner context */
return {
// New structure fields
id: row.id,
workflowId: row.workflowId,
userId,
name: row.name,
description: row.details?.tagline ?? null,
author: row.creator?.name ?? 'Unknown',
authorType,
organizationId,
details: row.details as { tagline?: string; about?: string } | null,
creatorId: row.creatorId,
creator: row.creator
? {
id: row.creator.id,
name: row.creator.name,
profileImageUrl: row.creator.profileImageUrl,
details: row.creator.details as {
about?: string
xUrl?: string
linkedinUrl?: string
websiteUrl?: string
contactEmail?: string
} | null,
referenceType: row.creator.referenceType,
referenceId: row.creator.referenceId,
}
: null,
views: row.views,
stars: row.stars,
color: '#3972F6', // default color for workspace cards
icon: 'Workflow', // default icon for workspace cards
status: row.status,
tags: row.tags,
requiredCredentials: row.requiredCredentials,
state: row.state as WorkspaceTemplate['state'],
createdAt: row.createdAt,
updatedAt: row.updatedAt,
isStarred: row.isStarred ?? false,
isSuperUser: effectiveSuperUser,
// Legacy fields for backward compatibility
userId,
description: (row.details as any)?.tagline ?? null,
author: row.creator?.name ?? 'Unknown',
authorType,
organizationId,
color: '#3972F6', // default color for workspace cards
icon: 'Workflow', // default icon for workspace cards
}
}) ?? []
@@ -2,37 +2,55 @@
import { useState } from 'react'
import { Layout, Search } from 'lucide-react'
import { Button } from '@/components/emcn'
import { Input } from '@/components/ui/input'
import { Button, Input } from '@/components/emcn'
import { createLogger } from '@/lib/logs/console/logger'
import {
TemplateCard,
TemplateCardSkeleton,
} from '@/app/workspace/[workspaceId]/templates/components/template-card'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import type { CreatorProfileDetails } from '@/types/creator-profile'
const logger = createLogger('TemplatesPage')
// Template data structure
/**
* Template data structure with support for both new and legacy fields
*/
export interface Template {
id: string
workflowId: string | null
userId: string
name: string
description: string | null
author: string
authorType: 'user' | 'organization'
organizationId: string | null
details?: {
tagline?: string
about?: string
} | null
creatorId: string | null
creator?: {
id: string
name: string
profileImageUrl?: string | null
details?: CreatorProfileDetails | null
referenceType: 'user' | 'organization'
referenceId: string
} | null
views: number
stars: number
color: string
icon: string
status: 'pending' | 'approved' | 'rejected'
tags: string[]
requiredCredentials: unknown
state: WorkflowState
createdAt: Date | string
updatedAt: Date | string
isStarred: boolean
isSuperUser?: boolean
// Legacy fields for backward compatibility with existing UI
userId?: string
description?: string | null
author?: string
authorType?: 'user' | 'organization'
organizationId?: string | null
color?: string
icon?: string
}
interface TemplatesProps {
@@ -41,21 +59,23 @@ interface TemplatesProps {
isSuperUser: boolean
}
/**
* Templates list component displaying workflow templates
* Supports filtering by tab (gallery/your/pending) and search
*/
export default function Templates({
initialTemplates,
currentUserId,
isSuperUser,
}: TemplatesProps) {
const [searchQuery, setSearchQuery] = useState('')
const [activeTab, setActiveTab] = useState('your')
const [activeTab, setActiveTab] = useState('gallery')
const [templates, setTemplates] = useState<Template[]>(initialTemplates)
const [loading, setLoading] = useState(false)
const handleTabClick = (tabId: string) => {
setActiveTab(tabId)
}
// Handle star change callback from template card
/**
* Update star status for a template
*/
const handleStarChange = (templateId: string, isStarred: boolean, newStarCount: number) => {
setTemplates((prevTemplates) =>
prevTemplates.map((template) =>
@@ -64,111 +84,90 @@ export default function Templates({
)
}
// Get templates for the active tab with search filtering
const getActiveTabTemplates = () => {
let filtered = templates
/**
* Filter templates based on active tab and search query
*/
const getFilteredTemplates = () => {
const query = searchQuery.toLowerCase()
// Filter by active tab
if (activeTab === 'your') {
filtered = filtered.filter(
(template) => template.userId === currentUserId || template.isStarred === true
)
} else if (activeTab === 'gallery') {
// Show all approved templates
filtered = filtered.filter((template) => template.status === 'approved')
} else if (activeTab === 'pending') {
// Show pending templates for super users
filtered = filtered.filter((template) => template.status === 'pending')
}
return templates.filter((template) => {
// Filter by tab
const tabMatch =
activeTab === 'your'
? template.userId === currentUserId || template.isStarred
: activeTab === 'gallery'
? template.status === 'approved'
: template.status === 'pending'
// Apply search filter
if (!tabMatch) return false
// Filter by search query
if (!query) return true
const searchableText = [
template.name,
template.description,
template.details?.tagline,
template.author,
template.creator?.name,
]
.filter(Boolean)
.join(' ')
.toLowerCase()
return searchableText.includes(query)
})
}
const filteredTemplates = getFilteredTemplates()
/**
* Get empty state message based on current filters
*/
const getEmptyStateMessage = () => {
if (searchQuery) {
filtered = filtered.filter(
(template) =>
template.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.author.toLowerCase().includes(searchQuery.toLowerCase())
)
return {
title: 'No templates found',
description: 'Try a different search term',
}
}
return filtered
const messages = {
pending: {
title: 'No pending templates',
description: 'New submissions will appear here',
},
your: {
title: 'No templates yet',
description: 'Create or star templates to see them here',
},
gallery: {
title: 'No templates available',
description: 'Templates will appear once approved',
},
}
return messages[activeTab as keyof typeof messages] || messages.gallery
}
const activeTemplates = getActiveTabTemplates()
// Helper function to render template cards
const renderTemplateCard = (template: Template) => (
<TemplateCard
key={template.id}
id={template.id}
title={template.name}
description={template.description || ''}
author={template.author}
usageCount={template.views.toString()}
stars={template.stars}
icon={template.icon}
iconColor={template.color}
state={template.state}
isStarred={template.isStarred}
onStarChange={handleStarChange}
isAuthenticated={true}
/>
)
// Render skeleton cards for loading state
const renderSkeletonCards = () => {
return Array.from({ length: 8 }).map((_, index) => (
<TemplateCardSkeleton key={`skeleton-${index}`} />
))
}
// Calculate counts for tabs
const yourTemplatesCount = templates.filter(
(template) => template.userId === currentUserId || template.isStarred === true
).length
const galleryCount = templates.filter((template) => template.status === 'approved').length
const pendingCount = templates.filter((template) => template.status === 'pending').length
const navigationTabs = [
{
id: 'gallery',
label: 'Gallery',
count: galleryCount,
},
{
id: 'your',
label: 'Your Templates',
count: yourTemplatesCount,
},
...(isSuperUser
? [
{
id: 'pending',
label: 'Pending',
count: pendingCount,
},
]
: []),
]
const emptyState = getEmptyStateMessage()
return (
<div className='flex h-[100vh] flex-col pl-64'>
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-auto px-[24px] pt-[24px] pb-[24px]'>
{/* Header */}
<div>
<div className='flex items-start gap-[12px]'>
<div className='flex h-[26px] w-[26px] items-center justify-center rounded-[6px] border border-[#7A5F11] bg-[#514215]'>
<Layout className='h-[14px] w-[14px] text-[#FBBC04]' />
<div className='flex h-[26px] w-[26px] items-center justify-center rounded-[6px] border border-[var(--brand-warning-border)] bg-[var(--brand-warning-bg)]'>
<Layout className='h-[14px] w-[14px] text-[var(--brand-warning)]' />
</div>
<h1 className='font-medium text-[18px]'>Templates</h1>
</div>
<p className='mt-[10px] font-base text-[#888888] text-[14px]'>
<p className='mt-[10px] font-base text-[14px] text-[var(--text-tertiary)]'>
Grab a template and start building, or make one from scratch.
</p>
</div>
{/* Search and Badges */}
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
@@ -183,52 +182,60 @@ export default function Templates({
<Button
variant={activeTab === 'gallery' ? 'active' : 'default'}
className='h-[32px] rounded-[6px]'
onClick={() => handleTabClick('gallery')}
onClick={() => setActiveTab('gallery')}
>
Gallery
</Button>
<Button
variant={activeTab === 'your' ? 'active' : 'default'}
className='h-[32px] rounded-[6px]'
onClick={() => handleTabClick('your')}
onClick={() => setActiveTab('your')}
>
Your Templates
</Button>
</div>
</div>
{/* Divider */}
<div className='mt-[24px] h-[1px] w-full border-[var(--border)] border-t' />
{/* Templates Grid - Based on Active Tab */}
<div className='mt-[24px] grid grid-cols-1 gap-x-[20px] gap-y-[40px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
{loading ? (
renderSkeletonCards()
) : activeTemplates.length === 0 ? (
<div className='col-span-full flex h-64 items-center justify-center rounded-lg border border-muted-foreground/25 border-dashed bg-muted/20'>
Array.from({ length: 8 }).map((_, index) => (
<TemplateCardSkeleton key={`skeleton-${index}`} />
))
) : filteredTemplates.length === 0 ? (
<div className='col-span-full flex h-64 items-center justify-center rounded-lg border border-[var(--border)] border-dashed bg-[var(--surface-3)]'>
<div className='text-center'>
<p className='font-medium text-muted-foreground text-sm'>
{searchQuery
? 'No templates found'
: activeTab === 'pending'
? 'No pending templates'
: activeTab === 'your'
? 'No templates yet'
: 'No templates available'}
<p className='font-medium text-[var(--text-secondary)] text-sm'>
{emptyState.title}
</p>
<p className='mt-1 text-muted-foreground/70 text-xs'>
{searchQuery
? 'Try a different search term'
: activeTab === 'pending'
? 'New submissions will appear here'
: activeTab === 'your'
? 'Create or star templates to see them here'
: 'Templates will appear once approved'}
<p className='mt-1 text-[var(--text-tertiary)] text-xs'>
{emptyState.description}
</p>
</div>
</div>
) : (
activeTemplates.map((template) => renderTemplateCard(template))
filteredTemplates.map((template) => {
const author = template.author || template.creator?.name || 'Unknown'
return (
<TemplateCard
key={template.id}
id={template.id}
title={template.name}
description={template.description || template.details?.tagline || ''}
author={author}
usageCount={template.views.toString()}
stars={template.stars}
icon={template.icon}
iconColor={template.color}
state={template.state}
isStarred={template.isStarred}
onStarChange={handleStarChange}
isAuthenticated={true}
/>
)
})
)}
</div>
</div>