diff --git a/web/app/components/plugins/marketplace/list/list-wrapper.tsx b/web/app/components/plugins/marketplace/list/list-wrapper.tsx index 7a3e822563..3646f5d032 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper.tsx @@ -12,6 +12,7 @@ import SortDropdown from '../sort-dropdown' import { useMarketplaceData } from '../state' import List from './index' import TemplateList from './template-list' +import TemplateSearchList from './template-search-list' type ListWrapperProps = { showInstallButton?: boolean @@ -40,7 +41,12 @@ const ListWrapper = ({ // Templates view if (creationType === 'templates') { - const { templateCollections, templateCollectionTemplatesMap } = marketplaceData + const { + templateCollections, + templateCollectionTemplatesMap, + templates, + isSearchMode: isTemplateSearchMode, + } = marketplaceData return (
+ isTemplateSearchMode + ? ( + + ) + : ( + + ) ) }
diff --git a/web/app/components/plugins/marketplace/list/template-card.tsx b/web/app/components/plugins/marketplace/list/template-card.tsx index 8c859bfe6c..84f8ec9806 100644 --- a/web/app/components/plugins/marketplace/list/template-card.tsx +++ b/web/app/components/plugins/marketplace/list/template-card.tsx @@ -4,33 +4,112 @@ import type { Template } from '../types' import { useLocale } from '#i18n' import Image from 'next/image' import * as React from 'react' +import { useCallback, useMemo } from 'react' +import useTheme from '@/hooks/use-theme' import { getLanguage } from '@/i18n-config/language' import { cn } from '@/utils/classnames' +import { getMarketplaceUrl } from '@/utils/var' type TemplateCardProps = { template: Template className?: string } +// Number of tag icons to show before showing "+X" +const MAX_VISIBLE_TAGS = 7 + +// Soft background color palette for avatar +const AVATAR_BG_COLORS = [ + 'bg-components-icon-bg-red-soft', + 'bg-components-icon-bg-orange-dark-soft', + 'bg-components-icon-bg-yellow-soft', + 'bg-components-icon-bg-green-soft', + 'bg-components-icon-bg-teal-soft', + 'bg-components-icon-bg-blue-light-soft', + 'bg-components-icon-bg-blue-soft', + 'bg-components-icon-bg-indigo-soft', + 'bg-components-icon-bg-violet-soft', + 'bg-components-icon-bg-pink-soft', +] + +// Simple hash function to get consistent color per template +const getAvatarBgClass = (id: string): string => { + let hash = 0 + for (let i = 0; i < id.length; i++) { + const char = id.charCodeAt(i) + hash = ((hash << 5) - hash) + char + hash = hash & hash + } + return AVATAR_BG_COLORS[Math.abs(hash) % AVATAR_BG_COLORS.length] +} + const TemplateCardComponent = ({ template, className, }: TemplateCardProps) => { const locale = useLocale() - const { name, description, icon, tags, author } = template + const { theme } = useTheme() + const { template_id, name, description, icon, tags, author, used_count, icon_background } = template as Template & { used_count?: number, icon_background?: string } + const isIconUrl = !!icon && /^(?:https?:)?\/\//.test(icon) + + const avatarBgStyle = useMemo(() => { + // If icon_background is provided (hex or rgba), use it directly + if (icon_background) + return { backgroundColor: icon_background } + return undefined + }, [icon_background]) + + const avatarBgClass = useMemo(() => { + // Only use class-based color if no inline style + if (icon_background) + return '' + return getAvatarBgClass(template_id) + }, [icon_background, template_id]) const descriptionText = description[getLanguage(locale)] || description.en_US || '' + const handleClick = useCallback(() => { + const url = getMarketplaceUrl(`/templates/${author}/${name}`, { + theme, + language: locale, + templateId: template_id, + }) + window.open(url, '_blank') + }, [author, name, theme, locale, template_id]) + + const visibleTags = tags?.slice(0, MAX_VISIBLE_TAGS) || [] + const remainingTagsCount = tags ? Math.max(0, tags.length - MAX_VISIBLE_TAGS) : 0 + + // Format used count (e.g., 134000 -> "134k") + const formatUsedCount = (count?: number) => { + if (!count) + return null + if (count >= 1000) + return `${Math.floor(count / 1000)}k` + return String(count) + } + + const formattedUsedCount = formatUsedCount(used_count) + return ( -
{/* Header */} -
-
- {icon +
+ {/* Avatar */} +
+ {isIconUrl ? ( ) : ( - ๐Ÿ“„ + {icon || '๐Ÿ“„'} )}
-
-
- {name} -
-
- by - {' '} - {author} + {/* Title */} +
+

{name}

+
+ + by + {author} + + {formattedUsedCount && ( + <> + ยท + + {formattedUsedCount} + {' '} + used + + + )}
{/* Description */} -
- {descriptionText} +
+

+ {descriptionText} +

- {/* Tags */} - {tags && tags.length > 0 && ( -
- {tags.slice(0, 3).map(tag => ( - - {tag} - - ))} - {tags.length > 3 && ( - - + - {tags.length - 3} - - )} -
- )} + {/* Bottom Info Bar - Tags as icons */} +
+ {tags && tags.length > 0 && ( + <> + {visibleTags.map((tag, index) => ( +
+ {tag} +
+ ))} + {remainingTagsCount > 0 && ( +
+ + + + {remainingTagsCount} + +
+ )} + + )} +
) } diff --git a/web/app/components/plugins/marketplace/list/template-search-list.tsx b/web/app/components/plugins/marketplace/list/template-search-list.tsx new file mode 100644 index 0000000000..086db7afa1 --- /dev/null +++ b/web/app/components/plugins/marketplace/list/template-search-list.tsx @@ -0,0 +1,27 @@ +'use client' + +import type { Template } from '../types' +import Empty from '../empty' +import TemplateCard from './template-card' + +type TemplateSearchListProps = { + templates: Template[] +} + +const TemplateSearchList = ({ templates }: TemplateSearchListProps) => { + if (templates.length === 0) { + return + } + + return ( +
+ {templates.map(template => ( +
+ +
+ ))} +
+ ) +} + +export default TemplateSearchList diff --git a/web/app/components/plugins/marketplace/state.ts b/web/app/components/plugins/marketplace/state.ts index 303ae168aa..89fa7637bf 100644 --- a/web/app/components/plugins/marketplace/state.ts +++ b/web/app/components/plugins/marketplace/state.ts @@ -6,7 +6,7 @@ import { useActivePluginType, useFilterPluginTags, useMarketplaceSearchMode, use import { PLUGIN_TYPE_SEARCH_MAP } from './constants' import { useMarketplaceContainerScroll } from './hooks' import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins, useMarketplaceTemplateCollectionsAndTemplates, useMarketplaceTemplates } from './query' -import { getCollectionsParams, getMarketplaceListFilterType } from './utils' +import { getCollectionsParams, getMarketplaceListFilterType, mapTemplateDetailToTemplate } from './utils' /** * Hook for plugins marketplace data @@ -122,13 +122,18 @@ export function useTemplatesMarketplaceData() { } }, [templateCollectionsQuery.data?.templateCollectionTemplatesMap]) + const searchTemplates = useMemo(() => { + const rawTemplates = templatesQuery.data?.pages.flatMap(page => page.templates) || [] + return rawTemplates.map(mapTemplateDetailToTemplate) + }, [templatesQuery.data]) + // Return search results when in search mode, otherwise return collection data if (isSearchMode) { return { isSearchMode, templateCollections: undefined, templateCollectionTemplatesMap: undefined, - templates: templatesQuery.data?.pages.flatMap(page => page.templates), + templates: searchTemplates, templatesTotal: templatesQuery.data?.pages[0]?.total, page: templatesQuery.data?.pages.length || 1, isLoading: templatesQuery.isLoading, diff --git a/web/app/components/plugins/marketplace/utils.ts b/web/app/components/plugins/marketplace/utils.ts index 34c287a86d..41995bfdc6 100644 --- a/web/app/components/plugins/marketplace/utils.ts +++ b/web/app/components/plugins/marketplace/utils.ts @@ -116,6 +116,23 @@ export const getMarketplaceCollectionsAndPlugins = async ( } } +export function mapTemplateDetailToTemplate(template: TemplateDetail): Template { + const descriptionText = template.overview || template.readme || '' + return { + template_id: template.id, + name: template.template_name, + description: { + en_US: descriptionText, + zh_Hans: descriptionText, + }, + icon: template.icon || '', + tags: template.categories || [], + author: template.publisher_unique_handle || template.creator_email || '', + created_at: template.created_at, + updated_at: template.updated_at, + } +} + export const getMarketplaceTemplateCollectionsAndTemplates = async ( query?: { page?: number, page_size?: number, condition?: string }, options?: MarketplaceFetchOptions, @@ -133,7 +150,7 @@ export const getMarketplaceTemplateCollectionsAndTemplates = async ( }, { signal: options?.signal, }) - templateCollections = res.data || [] + templateCollections = res.data?.collections || [] await Promise.all(templateCollections.map(async (collection) => { try { @@ -141,7 +158,8 @@ export const getMarketplaceTemplateCollectionsAndTemplates = async ( params: { collectionName: collection.name }, body: { limit: 20 }, }, { signal: options?.signal }) - templateCollectionTemplatesMap[collection.name] = (templatesRes.data || []) as Template[] + const templatesData = templatesRes.data?.templates || [] + templateCollectionTemplatesMap[collection.name] = templatesData.map(mapTemplateDetailToTemplate) } catch { templateCollectionTemplatesMap[collection.name] = [] diff --git a/web/contract/marketplace.ts b/web/contract/marketplace.ts index 4736e2c3f5..97d936ab31 100644 --- a/web/contract/marketplace.ts +++ b/web/contract/marketplace.ts @@ -10,7 +10,6 @@ import type { MarketplaceCollection, PluginsSearchParams, SyncCreatorProfileRequest, - Template, TemplateCollection, TemplateDetail, TemplateSearchParams, @@ -88,11 +87,13 @@ export const templateCollectionsContract = base ) .output( type<{ - data?: TemplateCollection[] - has_more?: boolean - limit?: number - page?: number - total?: number + data?: { + collections?: TemplateCollection[] + has_more?: boolean + limit?: number + page?: number + total?: number + } }>(), ) @@ -151,7 +152,7 @@ export const getCollectionTemplatesContract = base ) .output( type<{ - data?: Template[] + data?: TemplatesListResponse }>(), )