From 9f8289b185d8a8c94f8a9b89979648f8754e983b Mon Sep 17 00:00:00 2001 From: yessenia Date: Tue, 10 Feb 2026 20:57:53 +0800 Subject: [PATCH] feat: implement category switch components for marketplace with hero and default variants --- .../common.tsx} | 4 +- .../category-switch/hero-tags-filter.tsx | 94 +++++++++++++++++++ .../category-switch/hero-tags-trigger.tsx | 86 +++++++++++++++++ .../marketplace/category-switch/index.tsx | 4 + .../plugin.tsx} | 67 ++++++++----- .../template.tsx} | 13 +-- .../plugins/marketplace/description/index.tsx | 3 +- 7 files changed, 235 insertions(+), 36 deletions(-) rename web/app/components/plugins/marketplace/{category-switch.tsx => category-switch/common.tsx} (96%) create mode 100644 web/app/components/plugins/marketplace/category-switch/hero-tags-filter.tsx create mode 100644 web/app/components/plugins/marketplace/category-switch/hero-tags-trigger.tsx create mode 100644 web/app/components/plugins/marketplace/category-switch/index.tsx rename web/app/components/plugins/marketplace/{plugin-category-switch.tsx => category-switch/plugin.tsx} (57%) rename web/app/components/plugins/marketplace/{template-category-switch.tsx => category-switch/template.tsx} (86%) diff --git a/web/app/components/plugins/marketplace/category-switch.tsx b/web/app/components/plugins/marketplace/category-switch/common.tsx similarity index 96% rename from web/app/components/plugins/marketplace/category-switch.tsx rename to web/app/components/plugins/marketplace/category-switch/common.tsx index b3fece96b4..5e1ebc90b4 100644 --- a/web/app/components/plugins/marketplace/category-switch.tsx +++ b/web/app/components/plugins/marketplace/category-switch/common.tsx @@ -16,7 +16,7 @@ type CategorySwitchProps = { onChange: (value: string) => void } -const CategorySwitch = ({ +export const CommonCategorySwitch = ({ className, variant = 'default', options, @@ -62,5 +62,3 @@ const CategorySwitch = ({ ) } - -export default CategorySwitch diff --git a/web/app/components/plugins/marketplace/category-switch/hero-tags-filter.tsx b/web/app/components/plugins/marketplace/category-switch/hero-tags-filter.tsx new file mode 100644 index 0000000000..b437792dfc --- /dev/null +++ b/web/app/components/plugins/marketplace/category-switch/hero-tags-filter.tsx @@ -0,0 +1,94 @@ +'use client' + +import { useTranslation } from '#i18n' +import { useState } from 'react' +import Checkbox from '@/app/components/base/checkbox' +import Input from '@/app/components/base/input' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import { useTags } from '@/app/components/plugins/hooks' +import HeroTagsTrigger from './hero-tags-trigger' + +type HeroTagsFilterProps = { + tags: string[] + onTagsChange: (tags: string[]) => void +} + +const HeroTagsFilter = ({ + tags, + onTagsChange, +}: HeroTagsFilterProps) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + const [searchText, setSearchText] = useState('') + const { tags: options, tagsMap } = useTags() + const filteredOptions = options.filter(option => option.label.toLowerCase().includes(searchText.toLowerCase())) + const handleCheck = (id: string) => { + if (tags.includes(id)) + onTagsChange(tags.filter((tag: string) => tag !== id)) + else + onTagsChange([...tags, id]) + } + const selectedTagsLength = tags.length + + return ( + + setOpen(v => !v)} + > + + + +
+
+ setSearchText(e.target.value)} + placeholder={t('searchTags', { ns: 'pluginTags' }) || ''} + /> +
+
+ { + filteredOptions.map(option => ( +
handleCheck(option.name)} + > + +
+ {option.label} +
+
+ )) + } +
+
+
+
+ ) +} + +export default HeroTagsFilter diff --git a/web/app/components/plugins/marketplace/category-switch/hero-tags-trigger.tsx b/web/app/components/plugins/marketplace/category-switch/hero-tags-trigger.tsx new file mode 100644 index 0000000000..351da42ab7 --- /dev/null +++ b/web/app/components/plugins/marketplace/category-switch/hero-tags-trigger.tsx @@ -0,0 +1,86 @@ +'use client' + +import type { Tag } from '../../hooks' +import { useTranslation } from '#i18n' +import { RiArrowDownSLine, RiCloseCircleFill, RiPriceTag3Line } from '@remixicon/react' +import * as React from 'react' +import { cn } from '@/utils/classnames' + +type HeroTagsTriggerProps = { + selectedTagsLength: number + open: boolean + tags: string[] + tagsMap: Record + onTagsChange: (tags: string[]) => void +} + +const HeroTagsTrigger = ({ + selectedTagsLength, + open, + tags, + tagsMap, + onTagsChange, +}: HeroTagsTriggerProps) => { + const { t } = useTranslation() + const hasSelected = !!selectedTagsLength + + return ( +
+ +
+ { + !hasSelected && ( + {t('allTags', { ns: 'pluginTags' })} + ) + } + { + hasSelected && ( + + {tags.map(tag => tagsMap[tag]?.label).filter(Boolean).slice(0, 2).join(', ')} + + ) + } + { + selectedTagsLength > 2 && ( +
+ + + + {selectedTagsLength - 2} + +
+ ) + } +
+ { + hasSelected && ( + { + e.stopPropagation() + onTagsChange([]) + }} + /> + ) + } + { + !hasSelected && ( + + ) + } +
+ ) +} + +export default React.memo(HeroTagsTrigger) diff --git a/web/app/components/plugins/marketplace/category-switch/index.tsx b/web/app/components/plugins/marketplace/category-switch/index.tsx new file mode 100644 index 0000000000..b32b6d25a6 --- /dev/null +++ b/web/app/components/plugins/marketplace/category-switch/index.tsx @@ -0,0 +1,4 @@ +'use client' + +export { PluginCategorySwitch } from './plugin' +export { TemplateCategorySwitch } from './template' diff --git a/web/app/components/plugins/marketplace/plugin-category-switch.tsx b/web/app/components/plugins/marketplace/category-switch/plugin.tsx similarity index 57% rename from web/app/components/plugins/marketplace/plugin-category-switch.tsx rename to web/app/components/plugins/marketplace/category-switch/plugin.tsx index c9264fd790..ab3f5d6fb4 100644 --- a/web/app/components/plugins/marketplace/plugin-category-switch.tsx +++ b/web/app/components/plugins/marketplace/category-switch/plugin.tsx @@ -1,15 +1,16 @@ 'use client' -import type { ActivePluginType } from './constants' +import type { ActivePluginType } from '../constants' import type { PluginCategoryEnum } from '@/app/components/plugins/types' import { useTranslation } from '#i18n' import { RiArchive2Line } from '@remixicon/react' import { useSetAtom } from 'jotai' import { Plugin } from '@/app/components/base/icons/src/vender/plugin' -import { searchModeAtom, useActivePluginCategory } from './atoms' -import CategorySwitch from './category-switch' -import { PLUGIN_CATEGORY_WITH_COLLECTIONS, PLUGIN_TYPE_SEARCH_MAP } from './constants' -import { MARKETPLACE_TYPE_ICON_COMPONENTS } from './plugin-type-icons' +import { searchModeAtom, useActivePluginCategory, useFilterPluginTags } from '../atoms' +import { PLUGIN_CATEGORY_WITH_COLLECTIONS, PLUGIN_TYPE_SEARCH_MAP } from '../constants' +import { MARKETPLACE_TYPE_ICON_COMPONENTS } from '../plugin-type-icons' +import { CommonCategorySwitch } from './common' +import HeroTagsFilter from './hero-tags-filter' type PluginTypeSwitchProps = { className?: string @@ -25,12 +26,13 @@ const getTypeIcon = (value: ActivePluginType, isHeroVariant?: boolean) => { return Icon ? : null } -const PluginCategorySwitch = ({ +export const PluginCategorySwitch = ({ className, variant = 'default', }: PluginTypeSwitchProps) => { const { t } = useTranslation() const [activePluginCategory, handleActivePluginCategoryChange] = useActivePluginCategory() + const [filterPluginTags, setFilterPluginTags] = useFilterPluginTags() const setSearchMode = useSetAtom(searchModeAtom) const isHeroVariant = variant === 'hero' @@ -39,42 +41,42 @@ const PluginCategorySwitch = ({ { value: PLUGIN_TYPE_SEARCH_MAP.all, text: isHeroVariant ? t('category.allTypes', { ns: 'plugin' }) : t('category.all', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.all), + icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.all, isHeroVariant), }, { value: PLUGIN_TYPE_SEARCH_MAP.model, text: t('category.models', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.model), + icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.model, isHeroVariant), }, { value: PLUGIN_TYPE_SEARCH_MAP.tool, text: t('category.tools', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.tool), + icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.tool, isHeroVariant), }, { value: PLUGIN_TYPE_SEARCH_MAP.datasource, text: t('category.datasources', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.datasource), + icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.datasource, isHeroVariant), }, { value: PLUGIN_TYPE_SEARCH_MAP.trigger, text: t('category.triggers', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.trigger), + icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.trigger, isHeroVariant), }, { value: PLUGIN_TYPE_SEARCH_MAP.agent, text: t('category.agents', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.agent), + icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.agent, isHeroVariant), }, { value: PLUGIN_TYPE_SEARCH_MAP.extension, text: t('category.extensions', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.extension), + icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.extension, isHeroVariant), }, { value: PLUGIN_TYPE_SEARCH_MAP.bundle, text: t('category.bundles', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.bundle), + icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.bundle, isHeroVariant), }, ] @@ -85,15 +87,34 @@ const PluginCategorySwitch = ({ } } + if (!isHeroVariant) { + return ( + + ) + } + return ( - +
+ setFilterPluginTags(tags.length ? tags : null)} + /> +
+ ยท +
+ +
) } - -export default PluginCategorySwitch diff --git a/web/app/components/plugins/marketplace/template-category-switch.tsx b/web/app/components/plugins/marketplace/category-switch/template.tsx similarity index 86% rename from web/app/components/plugins/marketplace/template-category-switch.tsx rename to web/app/components/plugins/marketplace/category-switch/template.tsx index c07e14e5d2..946b01f43f 100644 --- a/web/app/components/plugins/marketplace/template-category-switch.tsx +++ b/web/app/components/plugins/marketplace/category-switch/template.tsx @@ -1,18 +1,17 @@ 'use client' import { useTranslation } from '#i18n' -import { RiFileList3Line } from '@remixicon/react' -import { useActiveTemplateCategory } from './atoms' -import CategorySwitch from './category-switch' -import { CATEGORY_ALL, TEMPLATE_CATEGORY_MAP } from './constants' import { Playground } from '@/app/components/base/icons/src/vender/plugin' +import { useActiveTemplateCategory } from '../atoms' +import { CATEGORY_ALL, TEMPLATE_CATEGORY_MAP } from '../constants' +import { CommonCategorySwitch } from './common' type TemplateCategorySwitchProps = { className?: string variant?: 'default' | 'hero' } -const TemplateCategorySwitch = ({ +export const TemplateCategorySwitch = ({ className, variant = 'default', }: TemplateCategorySwitchProps) => { @@ -65,7 +64,7 @@ const TemplateCategorySwitch = ({ ] return ( - ) } - -export default TemplateCategorySwitch diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx index 524153a4ad..ddfaf2e629 100644 --- a/web/app/components/plugins/marketplace/description/index.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -7,8 +7,7 @@ import { useEffect, useLayoutEffect, useRef } from 'react' import marketPlaceBg from '@/public/marketplace/hero-bg.jpg' import marketplaceGradientNoise from '@/public/marketplace/hero-gradient-noise.svg' import { cn } from '@/utils/classnames' -import PluginCategorySwitch from '../plugin-category-switch' -import TemplateCategorySwitch from '../template-category-switch' +import { PluginCategorySwitch, TemplateCategorySwitch } from '../category-switch/index' import { useMarketplaceData } from '../state' type DescriptionProps = {