11'use client' ;
22
3+ import { useMemo } from 'react' ;
4+
35import type { Project } from '@onlook/models' ;
6+ import { Button } from '@onlook/ui/button' ;
7+ import {
8+ DropdownMenu ,
9+ DropdownMenuContent ,
10+ DropdownMenuItem ,
11+ DropdownMenuTrigger ,
12+ } from '@onlook/ui/dropdown-menu' ;
13+ import { Icons } from '@onlook/ui/icons' ;
414import { timeAgo } from '@onlook/utility' ;
5- import { useMemo } from 'react' ;
615
716interface SquareProjectCardPresentationProps {
817 project : Project ;
@@ -12,6 +21,16 @@ interface SquareProjectCardPresentationProps {
1221 HighlightText ?: React . ComponentType < { text : string ; searchQuery : string } > ;
1322 /** Callback when card is clicked */
1423 onClick ?: ( project : Project ) => void ;
24+ /** Callback when rename is clicked */
25+ onRename ?: ( project : Project ) => void ;
26+ /** Callback when clone is clicked */
27+ onClone ?: ( project : Project ) => void ;
28+ /** Callback when convert to/from template is clicked */
29+ onToggleTemplate ?: ( project : Project ) => void ;
30+ /** Callback when delete is clicked */
31+ onDelete ?: ( project : Project ) => void ;
32+ /** Whether this project is a template */
33+ isTemplate ?: boolean ;
1534}
1635
1736/**
@@ -21,11 +40,19 @@ interface SquareProjectCardPresentationProps {
2140export function SquareProjectCardPresentation ( {
2241 project,
2342 imageUrl,
24- searchQuery = "" ,
43+ searchQuery = '' ,
2544 HighlightText,
2645 onClick,
46+ onRename,
47+ onClone,
48+ onToggleTemplate,
49+ onDelete,
50+ isTemplate = false ,
2751} : SquareProjectCardPresentationProps ) {
28- const lastUpdated = useMemo ( ( ) => timeAgo ( project . metadata . updatedAt ) , [ project . metadata . updatedAt ] ) ;
52+ const lastUpdated = useMemo (
53+ ( ) => timeAgo ( project . metadata . updatedAt ) ,
54+ [ project . metadata . updatedAt ] ,
55+ ) ;
2956
3057 const handleClick = ( ) => {
3158 onClick ?.( project ) ;
@@ -40,49 +67,142 @@ export function SquareProjectCardPresentation({
4067
4168 return (
4269 < div
43- className = "cursor-pointer transition-all duration-300 group "
70+ className = "group cursor-pointer transition-all duration-300"
4471 role = "button"
4572 tabIndex = { 0 }
4673 onClick = { handleClick }
4774 onKeyDown = { handleKeyDown }
4875 >
49- < div className = { `w-full aspect-[4/2.8] rounded-lg overflow-hidden relative shadow-sm transition-all duration-300` } >
76+ < div
77+ className = { `relative aspect-[4/2.8] w-full overflow-hidden rounded-lg shadow-sm transition-all duration-300` }
78+ >
5079 { imageUrl ? (
51- < img src = { imageUrl } alt = { project . name } className = "absolute inset-0 w-full h-full object-cover" loading = "lazy" />
80+ < img
81+ src = { imageUrl }
82+ alt = { project . name }
83+ className = "absolute inset-0 h-full w-full object-cover"
84+ loading = "lazy"
85+ />
5286 ) : (
5387 < >
54- < div className = "absolute inset-0 w-full h-full bg-gradient-to-t from-gray-800/40 via-gray-500/40 to-gray-400/40" />
55- < div className = "absolute inset-0 rounded-lg border-[0.5px] border-gray-500/70" style = { { maskImage : 'linear-gradient(to bottom, black 60%, transparent 100%)' } } />
88+ < div className = "absolute inset-0 h-full w-full bg-gradient-to-t from-gray-800/40 via-gray-500/40 to-gray-400/40" />
89+ < div
90+ className = "absolute inset-0 rounded-lg border-[0.5px] border-gray-500/70"
91+ style = { {
92+ maskImage :
93+ 'linear-gradient(to bottom, black 60%, transparent 100%)' ,
94+ } }
95+ />
5696 </ >
5797 ) }
5898
59- < div className = "absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
99+ < div className = "absolute inset-0 bg-black/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
100+
101+ < div className = "pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-black/70 to-transparent" />
60102
61- < div className = "absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-black/70 to-transparent pointer-events-none" />
103+ < div className = "absolute top-3 right-3 z-30 opacity-0 transition-opacity duration-200 group-hover:opacity-100" >
104+ < DropdownMenu >
105+ < DropdownMenuTrigger asChild >
106+ < Button
107+ size = "default"
108+ variant = "ghost"
109+ className = "hover:bg-background-onlook flex h-8 w-8 cursor-pointer items-center justify-center p-0 backdrop-blur-lg"
110+ onClick = { ( e ) => e . stopPropagation ( ) }
111+ >
112+ < Icons . DotsHorizontal />
113+ </ Button >
114+ </ DropdownMenuTrigger >
115+ < DropdownMenuContent
116+ className = "z-50"
117+ align = "end"
118+ alignOffset = { - 4 }
119+ sideOffset = { 8 }
120+ onClick = { ( e ) => e . stopPropagation ( ) }
121+ >
122+ { onRename && (
123+ < DropdownMenuItem
124+ onSelect = { ( event ) => {
125+ event . preventDefault ( ) ;
126+ onRename ( project ) ;
127+ } }
128+ className = "text-foreground-active hover:!bg-background-onlook hover:!text-foreground-active gap-2"
129+ >
130+ < Icons . Pencil className = "h-4 w-4" />
131+ Rename Project
132+ </ DropdownMenuItem >
133+ ) }
134+ { onClone && (
135+ < DropdownMenuItem
136+ onSelect = { ( event ) => {
137+ event . preventDefault ( ) ;
138+ onClone ( project ) ;
139+ } }
140+ className = "text-foreground-active hover:!bg-background-onlook hover:!text-foreground-active gap-2"
141+ >
142+ < Icons . Copy className = "h-4 w-4" />
143+ Clone Project
144+ </ DropdownMenuItem >
145+ ) }
146+ { onToggleTemplate && (
147+ < DropdownMenuItem
148+ onSelect = { ( event ) => {
149+ event . preventDefault ( ) ;
150+ onToggleTemplate ( project ) ;
151+ } }
152+ className = "text-foreground-active hover:!bg-background-onlook hover:!text-foreground-active gap-2"
153+ >
154+ { isTemplate ? (
155+ < >
156+ < Icons . CrossL className = "h-4 w-4 text-purple-600" />
157+ Unmark as template
158+ </ >
159+ ) : (
160+ < >
161+ < Icons . FilePlus className = "h-4 w-4" />
162+ Convert to template
163+ </ >
164+ ) }
165+ </ DropdownMenuItem >
166+ ) }
167+ { onDelete && (
168+ < DropdownMenuItem
169+ onSelect = { ( event ) => {
170+ event . preventDefault ( ) ;
171+ onDelete ( project ) ;
172+ } }
173+ className = "gap-2 text-red-400 hover:!bg-red-200/80 hover:!text-red-700 dark:text-red-200 dark:hover:!bg-red-800 dark:hover:!text-red-100"
174+ >
175+ < Icons . Trash className = "h-4 w-4" />
176+ Delete Project
177+ </ DropdownMenuItem >
178+ ) }
179+ </ DropdownMenuContent >
180+ </ DropdownMenu >
181+ </ div >
62182
63183 { onClick && (
64- < div className = "absolute inset-0 bg-background/30 opacity -0 group-hover:opacity-100 transition-opacity flex items-center justify-center z-30 " >
184+ < div className = "bg-background/30 absolute inset -0 z-30 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100 " >
65185 < button
66186 onClick = { ( e ) => {
67187 e . stopPropagation ( ) ;
68188 handleClick ( ) ;
69189 } }
70- className = "gap-2 border border-gray-300 w-auto cursor-pointer bg-white text-black hover:bg-gray-100 px-4 py-2 rounded "
190+ className = "w-auto cursor-pointer gap-2 rounded border border-gray-300 bg-white px-4 py-2 text-black hover:bg-gray-100"
71191 >
72192 ✏️ Edit
73193 </ button >
74194 </ div >
75195 ) }
76196
77- < div className = "absolute bottom-0 left-0 right-0 p-3 z-10 group-hover:opacity-50 transition-opacity duration-300 " >
78- < div className = "text-white font-medium text-sm mb-1 truncate drop-shadow-lg" >
197+ < div className = "absolute right-0 bottom-0 left-0 z-10 p-3 transition-opacity duration-300 group-hover:opacity-50" >
198+ < div className = "mb-1 truncate text-sm font-medium text-white drop-shadow-lg" >
79199 { HighlightText ? (
80200 < HighlightText text = { project . name } searchQuery = { searchQuery } />
81201 ) : (
82202 project . name
83203 ) }
84204 </ div >
85- < div className = "text-white/70 text-xs mb-1 drop-shadow-lg flex items-center " >
205+ < div className = "mb-1 flex items-center text-xs text-white/70 drop-shadow-lg" >
86206 < span > { lastUpdated } ago</ span >
87207 </ div >
88208 </ div >
0 commit comments