1+ import { useState } from 'react'
12import {
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'
4346import { cn } from '@/lib/utils'
4447import { getBlock } from '@/blocks/registry'
4548
49+ const logger = createLogger ( 'TemplateCard' )
50+
4651// Icon mapping for template icons
4752const 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 >
0 commit comments