diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx new file mode 100644 index 0000000000..f35f455618 --- /dev/null +++ b/web/app/components/plugins/marketplace/index.tsx @@ -0,0 +1,35 @@ +import SearchBox from './search-box' + +const Marketplace = () => { + return ( +
+

+ Empower your AI development +

+

+ Discover + + models + + , + + tools + + , + + extensions + + and + + bundles + + in Dify Marketplace +

+
+ {}} /> +
+
+ ) +} + +export default Marketplace diff --git a/web/app/components/plugins/marketplace/search-box/index.tsx b/web/app/components/plugins/marketplace/search-box/index.tsx new file mode 100644 index 0000000000..e29256b555 --- /dev/null +++ b/web/app/components/plugins/marketplace/search-box/index.tsx @@ -0,0 +1,50 @@ +'use client' +import { + useCallback, + useState, +} from 'react' +import { RiCloseLine } from '@remixicon/react' +import TagsFilter from './tags-filter' +import ActionButton from '@/app/components/base/action-button' + +type SearchBoxProps = { + onChange: (searchText: string, tags: string[]) => void +} +const SearchBox = ({ + onChange, +}: SearchBoxProps) => { + const [searchText, setSearchText] = useState('') + const [selectedTags, setSelectedTags] = useState([]) + + const handleTagsChange = useCallback((tags: string[]) => { + setSelectedTags(tags) + onChange(searchText, tags) + }, [searchText, onChange]) + + return ( +
+ +
+
+
+ { + setSearchText(e.target.value) + onChange(e.target.value, selectedTags) + }} + /> + setSearchText('')}> + + +
+
+
+ ) +} + +export default SearchBox diff --git a/web/app/components/plugins/marketplace/search-box/tags-filter.tsx b/web/app/components/plugins/marketplace/search-box/tags-filter.tsx new file mode 100644 index 0000000000..1615478af6 --- /dev/null +++ b/web/app/components/plugins/marketplace/search-box/tags-filter.tsx @@ -0,0 +1,135 @@ +'use client' + +import { useState } from 'react' +import { + RiArrowDownSLine, + RiCloseCircleFill, + RiFilter3Line, + RiSearchLine, +} from '@remixicon/react' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import Checkbox from '@/app/components/base/checkbox' +import cn from '@/utils/classnames' + +type TagsFilterProps = { + value: string[] + onChange: (tags: string[]) => void +} +const TagsFilter = ({ + value, + onChange, +}: TagsFilterProps) => { + const [open, setOpen] = useState(false) + const [searchText, setSearchText] = useState('') + const options = [ + { + value: 'search', + text: 'Search', + }, + { + value: 'image', + text: 'Image', + }, + ] + const filteredOptions = options.filter(option => option.text.toLowerCase().includes(searchText.toLowerCase())) + const handleCheck = (id: string) => { + if (value.includes(id)) + onChange(value.filter(tag => tag !== id)) + else + onChange([...value, id]) + } + const selectedTagsLength = value.length + + return ( + + setOpen(v => !v)}> +
+
+ +
+
+ { + !selectedTagsLength && 'All Tags' + } + { + !!selectedTagsLength && value.slice(0, 2).join(',') + } + { + selectedTagsLength > 2 && ( +
+ +{selectedTagsLength - 2} +
+ ) + } +
+ { + !!selectedTagsLength && ( + onChange([])} + /> + ) + } + { + !selectedTagsLength && ( + + ) + } +
+
+ +
+
+
+ + setSearchText(e.target.value)} + /> +
+
+
+ { + filteredOptions.map(option => ( +
+ handleCheck(option.value)} + /> +
+ {option.text} +
+
+ )) + } +
+
+
+
+ ) +} + +export default TagsFilter