Skip to content

Commit 72b68f7

Browse files
Add dropdown menu to recent projects cards
- Added dropdown menu with rename, clone, convert to template, and delete options to SquareProjectCardPresentation component - Updated SelectProjectPresentation to pass dropdown callbacks to recent projects cards - Matches functionality from ProjectCardPresentation in the Projects section Co-Authored-By: Satya Patel <[email protected]>
1 parent fe8d89d commit 72b68f7

File tree

2 files changed

+154
-27
lines changed

2 files changed

+154
-27
lines changed

apps/web/client/src/app/projects/_components/select-presentation.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
'use client';
22

33
import { useEffect, useMemo, useRef, useState } from 'react';
4-
import { Carousel } from './carousel';
54
import { AnimatePresence, motion } from 'motion/react';
65

76
import type { Project } from '@onlook/models';
87
import { Button } from '@onlook/ui/button';
98
import { Icons } from '@onlook/ui/icons';
109

11-
import { Templates } from './templates';
12-
import { TemplateModalPresentation } from './templates/template-modal-presentation';
10+
import { Carousel } from './carousel';
1311
import { HighlightText } from './select/highlight-text';
1412
import { MasonryLayout } from './select/masonry-layout';
1513
import { ProjectCardPresentation } from './select/project-card-presentation';
1614
import { SquareProjectCardPresentation } from './select/square-project-card-presentation';
15+
import { Templates } from './templates';
16+
import { TemplateModalPresentation } from './templates/template-modal-presentation';
1717

1818
interface SelectProjectPresentationProps {
1919
/** All projects including templates */
@@ -230,11 +230,7 @@ export const SelectProjectPresentation = ({
230230
Create a new project to get started
231231
</div>
232232
<div className="flex justify-center">
233-
<Button
234-
onClick={onCreateBlank}
235-
disabled={isCreatingProject}
236-
variant="default"
237-
>
233+
<Button onClick={onCreateBlank} disabled={isCreatingProject} variant="default">
238234
{isCreatingProject ? (
239235
<Icons.LoadingSpinner className="h-4 w-4 animate-spin" />
240236
) : (
@@ -308,7 +304,7 @@ export const SelectProjectPresentation = ({
308304
<button
309305
onClick={onCreateBlank}
310306
disabled={isCreatingProject}
311-
className="border-border bg-secondary/40 hover:bg-secondary relative flex aspect-[4/2.8] w-full items-center justify-center rounded-lg border transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
307+
className="border-border bg-secondary/40 hover:bg-secondary relative flex aspect-[4/2.8] w-full items-center justify-center rounded-lg border transition-colors disabled:cursor-not-allowed disabled:opacity-50"
312308
>
313309
<div className="text-foreground-tertiary flex flex-col items-center justify-center">
314310
{isCreatingProject ? (
@@ -350,9 +346,16 @@ export const SelectProjectPresentation = ({
350346
searchQuery={debouncedSearchQuery}
351347
HighlightText={HighlightText}
352348
onClick={onProjectClick}
349+
onRename={onRenameProject}
350+
onClone={onCloneProject}
351+
onToggleTemplate={onToggleTemplate}
352+
onDelete={onDeleteProject}
353+
isTemplate={project.metadata.tags.includes(
354+
'template',
355+
)}
353356
/>
354357
</motion.div>
355-
))
358+
)),
356359
]
357360
)}
358361
</AnimatePresence>
@@ -518,12 +521,16 @@ export const SelectProjectPresentation = ({
518521
}
519522
image={getImageUrl(selectedTemplate)}
520523
isNew={false}
521-
isStarred={selectedTemplate ? starredTemplateIds.has(selectedTemplate.id) : false}
524+
isStarred={
525+
selectedTemplate ? starredTemplateIds.has(selectedTemplate.id) : false
526+
}
522527
onToggleStar={() => selectedTemplate && handleToggleStar(selectedTemplate.id)}
523528
templateProject={selectedTemplate}
524529
onUnmarkTemplate={handleUnmarkTemplate}
525530
onUseTemplate={() => selectedTemplate && onUseTemplate?.(selectedTemplate.id)}
526-
onPreviewTemplate={() => selectedTemplate && onPreviewTemplate?.(selectedTemplate.id)}
531+
onPreviewTemplate={() =>
532+
selectedTemplate && onPreviewTemplate?.(selectedTemplate.id)
533+
}
527534
onEditTemplate={() => selectedTemplate && onEditTemplate?.(selectedTemplate.id)}
528535
/>
529536
)}

apps/web/client/src/app/projects/_components/select/square-project-card-presentation.tsx

Lines changed: 135 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
'use client';
22

3+
import { useMemo } from 'react';
4+
35
import 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';
414
import { timeAgo } from '@onlook/utility';
5-
import { useMemo } from 'react';
615

716
interface 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 {
2140
export 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

Comments
 (0)