- {blockTypes.length > 3 ? (
- <>
- {/* Show first 2 blocks when there are more than 3 */}
- {blockTypes.slice(0, 2).map((blockType, index) => {
+ {/* Title and Blocks Row */}
+
+ {/* Template Name */}
+
{title}
+
+ {/* Block Icons */}
+
+ {blockTypes.length > 4 ? (
+ <>
+ {/* Show first 3 blocks when there are more than 4 */}
+ {blockTypes.slice(0, 3).map((blockType, index) => {
+ const blockConfig = getBlockConfig(blockType)
+ if (!blockConfig) return null
+
+ return (
+
0 ? '-4px' : '0',
+ }}
+ >
+
+
+ )
+ })}
+ {/* Show +n for remaining blocks */}
+
+ +{blockTypes.length - 3}
+
+ >
+ ) : (
+ /* Show all blocks when 4 or fewer */
+ blockTypes.map((blockType, index) => {
const blockConfig = getBlockConfig(blockType)
if (!blockConfig) return null
return (
-
-
-
-
-
- )
- })}
- {/* Show +n block for remaining blocks */}
-
-
- +{blockTypes.length - 2}
-
-
- >
- ) : (
- /* Show all blocks when 3 or fewer */
- blockTypes.map((blockType, index) => {
- const blockConfig = getBlockConfig(blockType)
- if (!blockConfig) return null
-
- return (
-
0 ? '-4px' : '0',
}}
>
-
+
-
- )
- })
- )}
+ )
+ })
+ )}
+
+
+
+ {/* Creator and Stats Row */}
+
+ {/* Creator Info */}
+
+ {authorImageUrl ? (
+
+
+
+ ) : (
+
+
+
+ )}
+ {author}
+
+
+ {/* Stats */}
+
+
+ {usageCount}
+
+ {localStarCount}
+
)
}
+
+export const TemplateCard = memo(TemplateCardInner)
diff --git a/apps/sim/app/templates/layout-client.tsx b/apps/sim/app/templates/layout-client.tsx
new file mode 100644
index 0000000000..7051428881
--- /dev/null
+++ b/apps/sim/app/templates/layout-client.tsx
@@ -0,0 +1,12 @@
+'use client'
+
+import { Tooltip } from '@/components/emcn'
+import { season } from '@/app/fonts/season/season'
+
+export default function TemplatesLayoutClient({ children }: { children: React.ReactNode }) {
+ return (
+
+
{children}
+
+ )
+}
diff --git a/apps/sim/app/templates/layout.tsx b/apps/sim/app/templates/layout.tsx
index 306ad11c60..d1078c0793 100644
--- a/apps/sim/app/templates/layout.tsx
+++ b/apps/sim/app/templates/layout.tsx
@@ -1,12 +1,9 @@
-'use client'
-
-import { Tooltip } from '@/components/emcn'
-import { season } from '@/app/fonts/season/season'
+import TemplatesLayoutClient from './layout-client'
+/**
+ * Templates layout - server component wrapper for client layout.
+ * Redirect logic is handled by individual pages to preserve paths.
+ */
export default function TemplatesLayout({ children }: { children: React.ReactNode }) {
- return (
-
-
{children}
-
- )
+ return {children}
}
diff --git a/apps/sim/app/templates/page.tsx b/apps/sim/app/templates/page.tsx
index e159b42125..c233949a45 100644
--- a/apps/sim/app/templates/page.tsx
+++ b/apps/sim/app/templates/page.tsx
@@ -1,99 +1,66 @@
import { db } from '@sim/db'
-import { settings, templateCreators, templateStars, templates, user } from '@sim/db/schema'
-import { and, desc, eq, sql } from 'drizzle-orm'
+import { permissions, templateCreators, templates, workspace } from '@sim/db/schema'
+import { and, desc, eq } from 'drizzle-orm'
+import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import type { Template } from '@/app/templates/templates'
import Templates from '@/app/templates/templates'
+/**
+ * Public templates list page.
+ * Redirects authenticated users to their workspace-scoped templates page.
+ * Allows unauthenticated users to view templates for SEO and discovery.
+ */
export default async function TemplatesPage() {
const session = await getSession()
- // Check if user is a super user and if super user mode is enabled
- let effectiveSuperUser = false
+ // Authenticated users: redirect to workspace-scoped templates
if (session?.user?.id) {
- const currentUser = await db
- .select({ isSuperUser: user.isSuperUser })
- .from(user)
- .where(eq(user.id, session.user.id))
- .limit(1)
- const userSettings = await db
- .select({ superUserModeEnabled: settings.superUserModeEnabled })
- .from(settings)
- .where(eq(settings.userId, session.user.id))
+ const userWorkspaces = await db
+ .select({
+ workspace: workspace,
+ })
+ .from(permissions)
+ .innerJoin(workspace, eq(permissions.entityId, workspace.id))
+ .where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
+ .orderBy(desc(workspace.createdAt))
.limit(1)
- const isSuperUser = currentUser[0]?.isSuperUser || false
- const superUserModeEnabled = userSettings[0]?.superUserModeEnabled ?? true
-
- // Effective super user = database status AND UI mode enabled
- effectiveSuperUser = isSuperUser && superUserModeEnabled
+ if (userWorkspaces.length > 0) {
+ const firstWorkspace = userWorkspaces[0].workspace
+ redirect(`/workspace/${firstWorkspace.id}/templates`)
+ }
}
- // Fetch templates based on user status
- let templatesData
-
- if (session?.user?.id) {
- // Build where condition based on super user status
- const whereCondition = effectiveSuperUser ? undefined : eq(templates.status, 'approved')
-
- // Logged-in users: include star status
- templatesData = await db
- .select({
- id: templates.id,
- workflowId: templates.workflowId,
- name: templates.name,
- details: templates.details,
- creatorId: templates.creatorId,
- creator: templateCreators,
- views: templates.views,
- stars: templates.stars,
- status: templates.status,
- tags: templates.tags,
- requiredCredentials: templates.requiredCredentials,
- state: templates.state,
- createdAt: templates.createdAt,
- updatedAt: templates.updatedAt,
- isStarred: sql`CASE WHEN ${templateStars.id} IS NOT NULL THEN true ELSE false END`,
- })
- .from(templates)
- .leftJoin(
- templateStars,
- and(eq(templateStars.templateId, templates.id), eq(templateStars.userId, session.user.id))
- )
- .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
- .where(whereCondition)
- .orderBy(desc(templates.views), desc(templates.createdAt))
- } else {
- // Non-logged-in users: only approved templates, no star status
- templatesData = await db
- .select({
- id: templates.id,
- workflowId: templates.workflowId,
- name: templates.name,
- details: templates.details,
- creatorId: templates.creatorId,
- creator: templateCreators,
- views: templates.views,
- stars: templates.stars,
- status: templates.status,
- tags: templates.tags,
- requiredCredentials: templates.requiredCredentials,
- state: templates.state,
- createdAt: templates.createdAt,
- updatedAt: templates.updatedAt,
- })
- .from(templates)
- .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
- .where(eq(templates.status, 'approved'))
- .orderBy(desc(templates.views), desc(templates.createdAt))
- .then((rows) => rows.map((row) => ({ ...row, isStarred: false })))
- }
+ // Unauthenticated users: show public templates
+ const templatesData = await db
+ .select({
+ id: templates.id,
+ workflowId: templates.workflowId,
+ name: templates.name,
+ details: templates.details,
+ creatorId: templates.creatorId,
+ creator: templateCreators,
+ views: templates.views,
+ stars: templates.stars,
+ status: templates.status,
+ tags: templates.tags,
+ requiredCredentials: templates.requiredCredentials,
+ state: templates.state,
+ createdAt: templates.createdAt,
+ updatedAt: templates.updatedAt,
+ })
+ .from(templates)
+ .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
+ .where(eq(templates.status, 'approved'))
+ .orderBy(desc(templates.views), desc(templates.createdAt))
+ .then((rows) => rows.map((row) => ({ ...row, isStarred: false })))
return (
)
}
diff --git a/apps/sim/app/templates/templates.tsx b/apps/sim/app/templates/templates.tsx
index eb28fb5373..3ef375716a 100644
--- a/apps/sim/app/templates/templates.tsx
+++ b/apps/sim/app/templates/templates.tsx
@@ -1,13 +1,12 @@
'use client'
-import { useState } from 'react'
-import { ArrowLeft, Search } from 'lucide-react'
+import { useEffect, useMemo, useState } from 'react'
+import { Layout, Search } from 'lucide-react'
import { useRouter } from 'next/navigation'
-import { Button } from '@/components/ui/button'
+import { Button } from '@/components/emcn'
import { Input } from '@/components/ui/input'
import { createLogger } from '@/lib/logs/console/logger'
import type { CredentialRequirement } from '@/lib/workflows/credential-extractor'
-import { NavigationTabs } from '@/app/templates/components/navigation-tabs'
import { TemplateCard, TemplateCardSkeleton } from '@/app/templates/components/template-card'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import type { CreatorProfileDetails } from '@/types/creator-profile'
@@ -60,11 +59,30 @@ export default function Templates({
const [templates, setTemplates] = useState(initialTemplates)
const [loading, setLoading] = useState(false)
- const handleTabClick = (tabId: string) => {
- setActiveTab(tabId)
- }
+ // Redirect authenticated users to workspace templates
+ useEffect(() => {
+ if (currentUserId) {
+ const redirectToWorkspace = async () => {
+ try {
+ const response = await fetch('/api/workspaces')
+ if (response.ok) {
+ const data = await response.json()
+ const defaultWorkspace = data.workspaces?.[0]
+ if (defaultWorkspace) {
+ router.push(`/workspace/${defaultWorkspace.id}/templates`)
+ }
+ }
+ } catch (error) {
+ logger.error('Error redirecting to workspace:', error)
+ }
+ }
+ redirectToWorkspace()
+ }
+ }, [currentUserId, router])
- // 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) =>
@@ -73,239 +91,137 @@ export default function Templates({
)
}
- const matchesSearch = (template: Template) => {
- if (!searchQuery) return true
+ /**
+ * Filter templates based on active tab and search query
+ * Memoized to prevent unnecessary recalculations on render
+ */
+ const filteredTemplates = useMemo(() => {
const query = searchQuery.toLowerCase()
- return (
- template.name.toLowerCase().includes(query) ||
- template.details?.tagline?.toLowerCase().includes(query) ||
- template.creator?.name?.toLowerCase().includes(query)
- )
- }
- const ownedTemplates = currentUserId
- ? templates.filter(
- (template) =>
- template.creator?.referenceType === 'user' &&
- template.creator?.referenceId === currentUserId
- )
- : []
- const starredTemplates = currentUserId
- ? templates.filter(
- (template) =>
- template.isStarred &&
- !(
- template.creator?.referenceType === 'user' &&
- template.creator?.referenceId === currentUserId
- )
- )
- : []
+ return templates.filter((template) => {
+ // Filter by tab - only gallery and pending for public page
+ const tabMatch =
+ activeTab === 'gallery' ? template.status === 'approved' : template.status === 'pending'
- const filteredOwnedTemplates = ownedTemplates.filter(matchesSearch)
- const filteredStarredTemplates = starredTemplates.filter(matchesSearch)
+ if (!tabMatch) return false
- const galleryTemplates = templates
- .filter((template) => template.status === 'approved')
- .filter(matchesSearch)
+ // Filter by search query
+ if (!query) return true
- const pendingTemplates = templates
- .filter((template) => template.status === 'pending')
- .filter(matchesSearch)
+ const searchableText = [template.name, template.details?.tagline, template.creator?.name]
+ .filter(Boolean)
+ .join(' ')
+ .toLowerCase()
- // Helper function to render template cards
- const renderTemplateCard = (template: Template) => (
- }}
- isStarred={template.isStarred}
- onStarChange={handleStarChange}
- isAuthenticated={!!currentUserId}
- />
- )
+ return searchableText.includes(query)
+ })
+ }, [templates, activeTab, searchQuery])
- // Render skeleton cards for loading state
- const renderSkeletonCards = () => {
- return Array.from({ length: 8 }).map((_, index) => (
-
- ))
- }
-
- // Calculate counts for tabs
- const yourTemplatesCount = ownedTemplates.length + starredTemplates.length
- const galleryCount = templates.filter((template) => template.status === 'approved').length
- const pendingCount = templates.filter((template) => template.status === 'pending').length
-
- // Build tabs based on user status
- const navigationTabs = [
- {
- id: 'gallery',
- label: 'Gallery',
- count: galleryCount,
- },
- ...(currentUserId
- ? [
- {
- id: 'your',
- label: 'Your Templates',
- count: yourTemplatesCount,
- },
- ]
- : []),
- ...(isSuperUser
- ? [
- {
- id: 'pending',
- label: 'Pending',
- count: pendingCount,
- },
- ]
- : []),
- ]
-
- // Show tabs if there's more than one tab
- const showTabs = navigationTabs.length > 1
-
- const handleBackToWorkspace = async () => {
- try {
- const response = await fetch('/api/workspaces')
- if (response.ok) {
- const data = await response.json()
- const defaultWorkspace = data.workspaces?.[0]
- if (defaultWorkspace) {
- router.push(`/workspace/${defaultWorkspace.id}`)
- }
+ /**
+ * Get empty state message based on current filters
+ * Memoized to prevent unnecessary recalculations on render
+ */
+ const emptyState = useMemo(() => {
+ if (searchQuery) {
+ return {
+ title: 'No templates found',
+ description: 'Try a different search term',
}
- } catch (error) {
- logger.error('Error navigating to workspace:', error)
}
- }
+
+ const messages = {
+ pending: {
+ title: 'No pending templates',
+ description: 'New submissions will appear here',
+ },
+ gallery: {
+ title: 'No templates available',
+ description: 'Templates will appear once approved',
+ },
+ }
+
+ return messages[activeTab as keyof typeof messages] || messages.gallery
+ }, [searchQuery, activeTab])
return (
-
- {/* Header with Back Button */}
-
- {currentUserId && (
-
- )}
-
- Templates
-
-
- Grab a template and start building, or make
-
- one from scratch.
+
+
+
+
+
+
+
Templates
+
+
+ Grab a template and start building, or make one from scratch.