Skip to content

Commit b13f339

Browse files
authored
feat(sidebar): sidebar toggle and search (#700)
* fix: sidebar toggle * feat: search complete
1 parent ca4b483 commit b13f339

File tree

8 files changed

+960
-176
lines changed

8 files changed

+960
-176
lines changed

apps/sim/app/api/templates/[id]/use/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
4343
name: templates.name,
4444
description: templates.description,
4545
state: templates.state,
46+
color: templates.color,
4647
})
4748
.from(templates)
4849
.where(eq(templates.id, id))
@@ -80,6 +81,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
8081
name: `${templateData.name} (copy)`,
8182
description: templateData.description,
8283
state: templateData.state,
84+
color: templateData.color,
8385
userId: session.user.id,
8486
createdAt: now,
8587
updatedAt: now,

apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useState } from 'react'
12
import {
23
Award,
34
BarChart3,
@@ -40,9 +41,13 @@ import {
4041
Wrench,
4142
Zap,
4243
} from 'lucide-react'
44+
import { useParams, useRouter } from 'next/navigation'
45+
import { createLogger } from '@/lib/logs/console-logger'
4346
import { cn } from '@/lib/utils'
4447
import { getBlock } from '@/blocks/registry'
4548

49+
const logger = createLogger('TemplateCard')
50+
4651
// Icon mapping for template icons
4752
const iconMap = {
4853
// Content & Documentation
@@ -120,10 +125,11 @@ interface TemplateCardProps {
120125
state?: {
121126
blocks?: Record<string, { type: string; name?: string }>
122127
}
123-
// Add handlers for star and use actions
124-
onStar?: (templateId: string, isCurrentlyStarred: boolean) => Promise<void>
125-
onUse?: (templateId: string) => Promise<void>
126128
isStarred?: boolean
129+
// Optional callback when template is successfully used (for closing modals, etc.)
130+
onTemplateUsed?: () => void
131+
// Callback when star state changes (for parent state updates)
132+
onStarChange?: (templateId: string, isStarred: boolean, newStarCount: number) => void
127133
}
128134

129135
// Skeleton component for loading states
@@ -225,10 +231,18 @@ export function TemplateCard({
225231
onClick,
226232
className,
227233
state,
228-
onStar,
229-
onUse,
230234
isStarred = false,
235+
onTemplateUsed,
236+
onStarChange,
231237
}: TemplateCardProps) {
238+
const router = useRouter()
239+
const params = useParams()
240+
241+
// Local state for optimistic updates
242+
const [localIsStarred, setLocalIsStarred] = useState(isStarred)
243+
const [localStarCount, setLocalStarCount] = useState(stars)
244+
const [isStarLoading, setIsStarLoading] = useState(false)
245+
232246
// Extract block types from state if provided, otherwise use the blocks prop
233247
// Filter out starter blocks in both cases and sort for consistent rendering
234248
const blockTypes = state
@@ -238,19 +252,98 @@ export function TemplateCard({
238252
// Get the icon component
239253
const iconComponent = getIconComponent(icon)
240254

241-
// Handle star toggle
255+
// Handle star toggle with optimistic updates
242256
const handleStarClick = async (e: React.MouseEvent) => {
243257
e.stopPropagation()
244-
if (onStar) {
245-
await onStar(id, isStarred)
258+
259+
// Prevent multiple clicks while loading
260+
if (isStarLoading) return
261+
262+
setIsStarLoading(true)
263+
264+
// Optimistic update - update UI immediately
265+
const newIsStarred = !localIsStarred
266+
const newStarCount = newIsStarred ? localStarCount + 1 : localStarCount - 1
267+
268+
setLocalIsStarred(newIsStarred)
269+
setLocalStarCount(newStarCount)
270+
271+
// Notify parent component immediately for optimistic update
272+
if (onStarChange) {
273+
onStarChange(id, newIsStarred, newStarCount)
274+
}
275+
276+
try {
277+
const method = localIsStarred ? 'DELETE' : 'POST'
278+
const response = await fetch(`/api/templates/${id}/star`, { method })
279+
280+
if (!response.ok) {
281+
// Rollback on error
282+
setLocalIsStarred(localIsStarred)
283+
setLocalStarCount(localStarCount)
284+
285+
// Rollback parent state too
286+
if (onStarChange) {
287+
onStarChange(id, localIsStarred, localStarCount)
288+
}
289+
290+
logger.error('Failed to toggle star:', response.statusText)
291+
}
292+
} catch (error) {
293+
// Rollback on error
294+
setLocalIsStarred(localIsStarred)
295+
setLocalStarCount(localStarCount)
296+
297+
// Rollback parent state too
298+
if (onStarChange) {
299+
onStarChange(id, localIsStarred, localStarCount)
300+
}
301+
302+
logger.error('Error toggling star:', error)
303+
} finally {
304+
setIsStarLoading(false)
246305
}
247306
}
248307

249308
// Handle use template
250309
const handleUseClick = async (e: React.MouseEvent) => {
251310
e.stopPropagation()
252-
if (onUse) {
253-
await onUse(id)
311+
try {
312+
const response = await fetch(`/api/templates/${id}/use`, {
313+
method: 'POST',
314+
headers: {
315+
'Content-Type': 'application/json',
316+
},
317+
body: JSON.stringify({
318+
workspaceId: params.workspaceId,
319+
}),
320+
})
321+
322+
if (response.ok) {
323+
const data = await response.json()
324+
logger.info('Template use API response:', data)
325+
326+
if (!data.workflowId) {
327+
logger.error('No workflowId returned from API:', data)
328+
return
329+
}
330+
331+
const workflowUrl = `/workspace/${params.workspaceId}/w/${data.workflowId}`
332+
logger.info('Template used successfully, navigating to:', workflowUrl)
333+
334+
// Call the callback if provided (for closing modals, etc.)
335+
if (onTemplateUsed) {
336+
onTemplateUsed()
337+
}
338+
339+
// Use window.location.href for more reliable navigation
340+
window.location.href = workflowUrl
341+
} else {
342+
const errorText = await response.text()
343+
logger.error('Failed to use template:', response.statusText, errorText)
344+
}
345+
} catch (error) {
346+
logger.error('Error using template:', error)
254347
}
255348
}
256349

@@ -265,7 +358,7 @@ export function TemplateCard({
265358
{/* Left side - Info */}
266359
<div className='flex min-w-0 flex-1 flex-col justify-between p-4'>
267360
{/* Top section */}
268-
<div className='space-y-3'>
361+
<div className='space-y-2'>
269362
<div className='flex min-w-0 items-center justify-between gap-2.5'>
270363
<div className='flex min-w-0 items-center gap-2.5'>
271364
{/* Icon container */}
@@ -293,10 +386,11 @@ export function TemplateCard({
293386
<Star
294387
onClick={handleStarClick}
295388
className={cn(
296-
'h-4 w-4 cursor-pointer transition-colors',
297-
isStarred
389+
'h-4 w-4 cursor-pointer transition-all duration-200',
390+
localIsStarred
298391
? 'fill-yellow-400 text-yellow-400'
299-
: 'text-muted-foreground hover:fill-yellow-400 hover:text-yellow-400'
392+
: 'text-muted-foreground hover:fill-yellow-400 hover:text-yellow-400',
393+
isStarLoading && 'opacity-50'
300394
)}
301395
/>
302396
<button
@@ -319,7 +413,7 @@ export function TemplateCard({
319413
</div>
320414

321415
{/* Bottom section */}
322-
<div className='flex min-w-0 items-center gap-1.5 font-sans text-muted-foreground text-xs'>
416+
<div className='flex min-w-0 items-center gap-1.5 pt-1.5 font-sans text-muted-foreground text-xs'>
323417
<span className='flex-shrink-0'>by</span>
324418
<span className='min-w-0 truncate'>{author}</span>
325419
<span className='flex-shrink-0'></span>
@@ -329,7 +423,7 @@ export function TemplateCard({
329423
<div className='hidden flex-shrink-0 items-center gap-1.5 sm:flex'>
330424
<span></span>
331425
<Star className='h-3 w-3' />
332-
<span>{stars}</span>
426+
<span>{localStarCount}</span>
333427
</div>
334428
</div>
335429
</div>

apps/sim/app/workspace/[workspaceId]/templates/templates.tsx

Lines changed: 8 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -90,75 +90,18 @@ export default function Templates({ initialTemplates, currentUserId }: Templates
9090
}
9191
}
9292

93-
const handleTemplateClick = (templateId: string) => {
94-
// Navigate to template detail page
95-
router.push(`/workspace/${params.workspaceId}/templates/${templateId}`)
96-
}
97-
98-
// Handle using a template
99-
const handleUseTemplate = async (templateId: string) => {
100-
try {
101-
const response = await fetch(`/api/templates/${templateId}/use`, {
102-
method: 'POST',
103-
headers: {
104-
'Content-Type': 'application/json',
105-
},
106-
body: JSON.stringify({
107-
workspaceId: params.workspaceId,
108-
}),
109-
})
110-
111-
if (response.ok) {
112-
const data = await response.json()
113-
logger.info('Template use API response:', data)
114-
115-
if (!data.workflowId) {
116-
logger.error('No workflowId returned from API:', data)
117-
return
118-
}
119-
120-
const workflowUrl = `/workspace/${params.workspaceId}/w/${data.workflowId}`
121-
logger.info('Template used successfully, navigating to:', workflowUrl)
122-
123-
// Use window.location.href for more reliable navigation
124-
window.location.href = workflowUrl
125-
} else {
126-
const errorText = await response.text()
127-
logger.error('Failed to use template:', response.statusText, errorText)
128-
}
129-
} catch (error) {
130-
logger.error('Error using template:', error)
131-
}
132-
}
133-
13493
const handleCreateNew = () => {
13594
// TODO: Open create template modal or navigate to create page
13695
console.log('Create new template')
13796
}
13897

139-
// Handle starring/unstarring templates (client-side for interactivity)
140-
const handleStarToggle = async (templateId: string, isCurrentlyStarred: boolean) => {
141-
try {
142-
const method = isCurrentlyStarred ? 'DELETE' : 'POST'
143-
const response = await fetch(`/api/templates/${templateId}/star`, { method })
144-
145-
if (response.ok) {
146-
// Update local state optimistically
147-
setTemplates((prev) =>
148-
prev.map((template) =>
149-
template.id === templateId
150-
? {
151-
...template,
152-
isStarred: !isCurrentlyStarred,
153-
stars: isCurrentlyStarred ? template.stars - 1 : template.stars + 1,
154-
}
155-
: template
156-
)
157-
)
158-
}
159-
} catch (error) {
160-
logger.error('Error toggling star:', error)
161-
}
98+
// Handle star change callback from template card
99+
const handleStarChange = (templateId: string, isStarred: boolean, newStarCount: number) => {
100+
setTemplates((prevTemplates) =>
101+
prevTemplates.map((template) =>
102+
template.id === templateId ? { ...template, isStarred, stars: newStarCount } : template
103+
)
104+
)
162105
}
163106

164107
const filteredTemplates = (category: CategoryValue | 'your' | 'recent') => {
@@ -201,10 +144,8 @@ export default function Templates({ initialTemplates, currentUserId }: Templates
201144
icon={template.icon}
202145
iconColor={template.color}
203146
state={template.state as { blocks?: Record<string, { type: string; name?: string }> }}
204-
onClick={() => handleTemplateClick(template.id)}
205-
onStar={handleStarToggle}
206-
onUse={handleUseTemplate}
207147
isStarred={template.isStarred}
148+
onStarChange={handleStarChange}
208149
/>
209150
)
210151

0 commit comments

Comments
 (0)