diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index 72c42da49..48803d0e7 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -1,5 +1,6 @@ import { BookText, + Bookmark, CheckIcon, ChevronsUpDown, Globe, @@ -82,6 +83,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => { const [open, setOpen] = useState(false); const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed"); const [selectedTags, setSelectedTags] = useState([]); + const [showFavoritesOnly, setShowFavoritesOnly] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(() => { // Try to get from props first, then localStorage if (baseUrl) return baseUrl; @@ -122,8 +124,45 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => { enabled: open, }, ); + + const { data: favoriteIds = [], isLoading: isLoadingFavorites } = + api.user.getBookmarkedTemplates.useQuery(undefined, { + enabled: open, + }); + const utils = api.useUtils(); + const { mutateAsync: toggleFavorite } = + api.user.toggleTemplateBookmark.useMutation({ + onMutate: async ({ templateId }) => { + await utils.user.getBookmarkedTemplates.cancel(); + const previousFavorites = utils.user.getBookmarkedTemplates.getData(); + + utils.user.getBookmarkedTemplates.setData(undefined, (old = []) => { + if (old.includes(templateId)) { + return old.filter((id) => id !== templateId); + } + return [...old, templateId]; + }); + + return { previousFavorites }; + }, + onError: (err, variables, context) => { + if (context?.previousFavorites) { + utils.user.getBookmarkedTemplates.setData( + undefined, + context.previousFavorites, + ); + } + toast.error("Failed to update favorite"); + }, + onSuccess: (data) => { + toast.success( + data.isBookmarked ? "Added to favorites" : "Removed from favorites", + ); + }, + }); + const [serverId, setServerId] = useState(undefined); const { mutateAsync, isLoading, error, isError } = api.compose.deployTemplate.useMutation(); @@ -137,7 +176,9 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => { query === "" || template.name.toLowerCase().includes(query.toLowerCase()) || template.description.toLowerCase().includes(query.toLowerCase()); - return matchesTags && matchesQuery; + const matchesFavorites = + !showFavoritesOnly || favoriteIds.includes(template.id); + return matchesTags && matchesQuery && matchesFavorites; }) || []; const hasServers = servers && servers.length > 0; @@ -146,6 +187,14 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => { // Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers) const shouldShowServerDropdown = hasServers; + const handleToggleFavorite = async ( + e: React.MouseEvent, + templateId: string, + ) => { + e.stopPropagation(); + await toggleFavorite({ templateId }); + }; + return ( @@ -243,6 +292,20 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => { + + +
+ {template?.version} +