feat: marketplace list
This commit is contained in:
parent
ca9e23d6ea
commit
9a65c3391b
@ -2,21 +2,42 @@
|
|||||||
|
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
|
useCallback,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useContextSelector,
|
useContextSelector,
|
||||||
} from 'use-context-selector'
|
} from 'use-context-selector'
|
||||||
|
import { useDebounceFn } from 'ahooks'
|
||||||
|
import { PLUGIN_TYPE_SEARCH_MAP } from './plugin-type-switch'
|
||||||
|
import type { Plugin } from '../types'
|
||||||
|
import type { PluginsSearchParams } from './types'
|
||||||
|
|
||||||
export type MarketplaceContextValue = {
|
export type MarketplaceContextValue = {
|
||||||
intersected: boolean
|
intersected: boolean
|
||||||
setIntersected: (intersected: boolean) => void
|
setIntersected: (intersected: boolean) => void
|
||||||
|
searchPluginText: string
|
||||||
|
handleSearchPluginTextChange: (text: string) => void
|
||||||
|
filterPluginTags: string[]
|
||||||
|
handleFilterPluginTagsChange: (tags: string[]) => void
|
||||||
|
activePluginType: string
|
||||||
|
handleActivePluginTypeChange: (type: string) => void
|
||||||
|
plugins?: Plugin[]
|
||||||
|
setPlugins?: (plugins: Plugin[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MarketplaceContext = createContext<MarketplaceContextValue>({
|
export const MarketplaceContext = createContext<MarketplaceContextValue>({
|
||||||
intersected: true,
|
intersected: true,
|
||||||
setIntersected: () => {},
|
setIntersected: () => {},
|
||||||
|
searchPluginText: '',
|
||||||
|
handleSearchPluginTextChange: () => {},
|
||||||
|
filterPluginTags: [],
|
||||||
|
handleFilterPluginTagsChange: () => {},
|
||||||
|
activePluginType: PLUGIN_TYPE_SEARCH_MAP.all,
|
||||||
|
handleActivePluginTypeChange: () => {},
|
||||||
|
plugins: undefined,
|
||||||
|
setPlugins: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
type MarketplaceContextProviderProps = {
|
type MarketplaceContextProviderProps = {
|
||||||
@ -31,12 +52,69 @@ export const MarketplaceContextProvider = ({
|
|||||||
children,
|
children,
|
||||||
}: MarketplaceContextProviderProps) => {
|
}: MarketplaceContextProviderProps) => {
|
||||||
const [intersected, setIntersected] = useState(true)
|
const [intersected, setIntersected] = useState(true)
|
||||||
|
const [searchPluginText, setSearchPluginText] = useState('')
|
||||||
|
const [filterPluginTags, setFilterPluginTags] = useState<string[]>([])
|
||||||
|
const [activePluginType, setActivePluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all)
|
||||||
|
const [plugins, setPlugins] = useState<Plugin[]>()
|
||||||
|
|
||||||
|
const handleUpdatePlugins = useCallback((query: PluginsSearchParams) => {
|
||||||
|
const fetchPlugins = async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://marketplace.dify.dev/api/v1/plugins/search/basic',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: query.query,
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
sort_by: query.sortBy,
|
||||||
|
sort_order: query.sortOrder,
|
||||||
|
category: query.category,
|
||||||
|
tag: query.tag,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const data = await response.json()
|
||||||
|
setPlugins(data.data.plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPlugins()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { run: handleUpdatePluginsWithDebounced } = useDebounceFn(handleUpdatePlugins, {
|
||||||
|
wait: 500,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearchPluginTextChange = useCallback((text: string) => {
|
||||||
|
setSearchPluginText(text)
|
||||||
|
|
||||||
|
handleUpdatePluginsWithDebounced({ query: text })
|
||||||
|
}, [handleUpdatePluginsWithDebounced])
|
||||||
|
|
||||||
|
const handleFilterPluginTagsChange = useCallback((tags: string[]) => {
|
||||||
|
setFilterPluginTags(tags)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleActivePluginTypeChange = useCallback((type: string) => {
|
||||||
|
setActivePluginType(type)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MarketplaceContext.Provider
|
<MarketplaceContext.Provider
|
||||||
value={{
|
value={{
|
||||||
intersected,
|
intersected,
|
||||||
setIntersected,
|
setIntersected,
|
||||||
|
searchPluginText,
|
||||||
|
handleSearchPluginTextChange,
|
||||||
|
filterPluginTags,
|
||||||
|
handleFilterPluginTagsChange,
|
||||||
|
activePluginType,
|
||||||
|
handleActivePluginTypeChange,
|
||||||
|
plugins,
|
||||||
|
setPlugins,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { Plugin } from '@/app/components/plugins/types'
|
|||||||
import Card from '@/app/components/plugins/card'
|
import Card from '@/app/components/plugins/card'
|
||||||
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
||||||
|
|
||||||
interface ListWithCollectionProps {
|
type ListWithCollectionProps = {
|
||||||
marketplaceCollections: MarketplaceCollection[]
|
marketplaceCollections: MarketplaceCollection[]
|
||||||
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
|
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { Plugin } from '../../types'
|
import type { Plugin } from '../../types'
|
||||||
import type { MarketplaceCollection } from '../types'
|
import type { MarketplaceCollection } from '../types'
|
||||||
|
import { useMarketplaceContext } from '../context'
|
||||||
import List from './index'
|
import List from './index'
|
||||||
|
|
||||||
interface ListWrapperProps {
|
type ListWrapperProps = {
|
||||||
marketplaceCollections: MarketplaceCollection[]
|
marketplaceCollections: MarketplaceCollection[]
|
||||||
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
|
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
|
||||||
}
|
}
|
||||||
@ -11,10 +12,13 @@ const ListWrapper = ({
|
|||||||
marketplaceCollections,
|
marketplaceCollections,
|
||||||
marketplaceCollectionPluginsMap,
|
marketplaceCollectionPluginsMap,
|
||||||
}: ListWrapperProps) => {
|
}: ListWrapperProps) => {
|
||||||
|
const plugins = useMarketplaceContext(s => s.plugins)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
marketplaceCollections={marketplaceCollections}
|
marketplaceCollections={marketplaceCollections}
|
||||||
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
|
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
|
||||||
|
plugins={plugins}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,22 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { PluginType } from '../types'
|
|
||||||
import {
|
import {
|
||||||
RiArchive2Line,
|
RiArchive2Line,
|
||||||
RiBrain2Line,
|
RiBrain2Line,
|
||||||
RiHammerLine,
|
RiHammerLine,
|
||||||
RiPuzzle2Line,
|
RiPuzzle2Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
|
import { PluginType } from '../types'
|
||||||
|
import { useMarketplaceContext } from './context'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
const PLUGIN_TYPE_SEARCH_MAP = {
|
export const PLUGIN_TYPE_SEARCH_MAP = {
|
||||||
all: 'all',
|
all: 'all',
|
||||||
model: PluginType.model,
|
model: PluginType.model,
|
||||||
tool: PluginType.tool,
|
tool: PluginType.tool,
|
||||||
extension: PluginType.extension,
|
extension: PluginType.extension,
|
||||||
bundle: 'bundle',
|
bundle: 'bundle',
|
||||||
}
|
}
|
||||||
type PluginTypeSwitchProps = {
|
|
||||||
onChange?: (type: string) => void
|
|
||||||
}
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
value: PLUGIN_TYPE_SEARCH_MAP.all,
|
value: PLUGIN_TYPE_SEARCH_MAP.all,
|
||||||
@ -47,10 +44,9 @@ const options = [
|
|||||||
icon: <RiArchive2Line className='mr-1.5 w-4 h-4' />,
|
icon: <RiArchive2Line className='mr-1.5 w-4 h-4' />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const PluginTypeSwitch = ({
|
const PluginTypeSwitch = () => {
|
||||||
onChange,
|
const activePluginType = useMarketplaceContext(s => s.activePluginType)
|
||||||
}: PluginTypeSwitchProps) => {
|
const handleActivePluginTypeChange = useMarketplaceContext(s => s.handleActivePluginTypeChange)
|
||||||
const [activeType, setActiveType] = useState(PLUGIN_TYPE_SEARCH_MAP.all)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
@ -62,11 +58,10 @@ const PluginTypeSwitch = ({
|
|||||||
key={option.value}
|
key={option.value}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center px-3 h-8 border border-transparent rounded-xl cursor-pointer hover:bg-state-base-hover hover:text-text-secondary system-md-medium text-text-tertiary',
|
'flex items-center px-3 h-8 border border-transparent rounded-xl cursor-pointer hover:bg-state-base-hover hover:text-text-secondary system-md-medium text-text-tertiary',
|
||||||
activeType === option.value && 'border-components-main-nav-nav-button-border !bg-components-main-nav-nav-button-bg-active !text-components-main-nav-nav-button-text-active shadow-xs',
|
activePluginType === option.value && 'border-components-main-nav-nav-button-border !bg-components-main-nav-nav-button-bg-active !text-components-main-nav-nav-button-text-active shadow-xs',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveType(option.value)
|
handleActivePluginTypeChange(option.value)
|
||||||
onChange?.(option.value)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.icon}
|
{option.icon}
|
||||||
|
|||||||
@ -1,29 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import {
|
|
||||||
useCallback,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
import { useMarketplaceContext } from '../context'
|
import { useMarketplaceContext } from '../context'
|
||||||
import TagsFilter from './tags-filter'
|
import TagsFilter from './tags-filter'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
type SearchBoxProps = {
|
const SearchBox = () => {
|
||||||
onChange?: (searchText: string, tags: string[]) => void
|
|
||||||
}
|
|
||||||
const SearchBox = ({
|
|
||||||
onChange,
|
|
||||||
}: SearchBoxProps) => {
|
|
||||||
const intersected = useMarketplaceContext(v => v.intersected)
|
const intersected = useMarketplaceContext(v => v.intersected)
|
||||||
const [searchText, setSearchText] = useState('')
|
const searchPluginText = useMarketplaceContext(v => v.searchPluginText)
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
const handleSearchPluginTextChange = useMarketplaceContext(v => v.handleSearchPluginTextChange)
|
||||||
|
|
||||||
const handleTagsChange = useCallback((tags: string[]) => {
|
|
||||||
setSelectedTags(tags)
|
|
||||||
onChange?.(searchText, tags)
|
|
||||||
}, [searchText, onChange])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -32,24 +17,20 @@ const SearchBox = ({
|
|||||||
!intersected && 'w-[508px] transition-[width] duration-300',
|
!intersected && 'w-[508px] transition-[width] duration-300',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TagsFilter
|
<TagsFilter />
|
||||||
value={selectedTags}
|
|
||||||
onChange={handleTagsChange}
|
|
||||||
/>
|
|
||||||
<div className='mx-1 w-[1px] h-3.5 bg-divider-regular'></div>
|
<div className='mx-1 w-[1px] h-3.5 bg-divider-regular'></div>
|
||||||
<div className='grow flex items-center p-1 pl-2'>
|
<div className='grow flex items-center p-1 pl-2'>
|
||||||
<div className='flex items-center mr-2 py-0.5 w-full'>
|
<div className='flex items-center mr-2 py-0.5 w-full'>
|
||||||
<input
|
<input
|
||||||
className='grow block outline-none appearance-none body-md-medium text-text-secondary'
|
className='grow block outline-none appearance-none body-md-medium text-text-secondary'
|
||||||
value={searchText}
|
value={searchPluginText}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchText(e.target.value)
|
handleSearchPluginTextChange(e.target.value)
|
||||||
onChange?.(e.target.value, selectedTags)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
searchText && (
|
searchPluginText && (
|
||||||
<ActionButton onClick={() => setSearchText('')}>
|
<ActionButton onClick={() => handleSearchPluginTextChange('')}>
|
||||||
<RiCloseLine className='w-4 h-4' />
|
<RiCloseLine className='w-4 h-4' />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
RiCloseCircleFill,
|
RiCloseCircleFill,
|
||||||
RiFilter3Line,
|
RiFilter3Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
|
import { useMarketplaceContext } from '../context'
|
||||||
import {
|
import {
|
||||||
PortalToFollowElem,
|
PortalToFollowElem,
|
||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
@ -15,14 +16,9 @@ import Checkbox from '@/app/components/base/checkbox'
|
|||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
|
|
||||||
type TagsFilterProps = {
|
const TagsFilter = () => {
|
||||||
value: string[]
|
const filterPluginTags = useMarketplaceContext(v => v.filterPluginTags)
|
||||||
onChange: (tags: string[]) => void
|
const handleFilterPluginTagsChange = useMarketplaceContext(v => v.handleFilterPluginTagsChange)
|
||||||
}
|
|
||||||
const TagsFilter = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: TagsFilterProps) => {
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
const options = [
|
const options = [
|
||||||
@ -37,12 +33,12 @@ const TagsFilter = ({
|
|||||||
]
|
]
|
||||||
const filteredOptions = options.filter(option => option.text.toLowerCase().includes(searchText.toLowerCase()))
|
const filteredOptions = options.filter(option => option.text.toLowerCase().includes(searchText.toLowerCase()))
|
||||||
const handleCheck = (id: string) => {
|
const handleCheck = (id: string) => {
|
||||||
if (value.includes(id))
|
if (filterPluginTags.includes(id))
|
||||||
onChange(value.filter(tag => tag !== id))
|
handleFilterPluginTagsChange(filterPluginTags.filter((tag: string) => tag !== id))
|
||||||
else
|
else
|
||||||
onChange([...value, id])
|
handleFilterPluginTagsChange([...filterPluginTags, id])
|
||||||
}
|
}
|
||||||
const selectedTagsLength = value.length
|
const selectedTagsLength = filterPluginTags.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PortalToFollowElem
|
<PortalToFollowElem
|
||||||
@ -70,7 +66,7 @@ const TagsFilter = ({
|
|||||||
!selectedTagsLength && 'All Tags'
|
!selectedTagsLength && 'All Tags'
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!!selectedTagsLength && value.slice(0, 2).join(',')
|
!!selectedTagsLength && filterPluginTags.slice(0, 2).join(',')
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
selectedTagsLength > 2 && (
|
selectedTagsLength > 2 && (
|
||||||
@ -84,7 +80,7 @@ const TagsFilter = ({
|
|||||||
!!selectedTagsLength && (
|
!!selectedTagsLength && (
|
||||||
<RiCloseCircleFill
|
<RiCloseCircleFill
|
||||||
className='w-4 h-4 text-text-quaternary cursor-pointer'
|
className='w-4 h-4 text-text-quaternary cursor-pointer'
|
||||||
onClick={() => onChange([])}
|
onClick={() => handleFilterPluginTagsChange([])}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -115,7 +111,7 @@ const TagsFilter = ({
|
|||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className='mr-1'
|
className='mr-1'
|
||||||
checked={value.includes(option.value)}
|
checked={filterPluginTags.includes(option.value)}
|
||||||
/>
|
/>
|
||||||
<div className='px-1 system-sm-medium text-text-secondary'>
|
<div className='px-1 system-sm-medium text-text-secondary'>
|
||||||
{option.text}
|
{option.text}
|
||||||
|
|||||||
@ -17,3 +17,13 @@ export type MarketplaceCollectionPluginsResponse = {
|
|||||||
plugins: Plugin[]
|
plugins: Plugin[]
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PluginsSearchParams = {
|
||||||
|
query: string
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
sortBy?: string
|
||||||
|
sortOrder?: string
|
||||||
|
category?: string
|
||||||
|
tag?: string
|
||||||
|
}
|
||||||
|
|||||||
@ -1,233 +0,0 @@
|
|||||||
import { RiArrowUpDoubleLine } from '@remixicon/react'
|
|
||||||
import Card from '@/app/components/plugins/card'
|
|
||||||
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
|
||||||
import { toolNotion } from '@/app/components/plugins/card/card-mock'
|
|
||||||
import { useGetLanguage } from '@/context/i18n'
|
|
||||||
|
|
||||||
type MarketplaceProps = {
|
|
||||||
onMarketplaceScroll: () => void
|
|
||||||
}
|
|
||||||
const Marketplace = ({
|
|
||||||
onMarketplaceScroll,
|
|
||||||
}: MarketplaceProps) => {
|
|
||||||
const locale = useGetLanguage()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='shrink-0 sticky -bottom-[442px] h-[530px] overflow-y-auto px-12 py-2 pt-0 bg-background-default-subtle'>
|
|
||||||
<RiArrowUpDoubleLine
|
|
||||||
className='absolute top-2 left-1/2 -translate-x-1/2 w-4 h-4 text-text-quaternary cursor-pointer'
|
|
||||||
onClick={() => onMarketplaceScroll()}
|
|
||||||
/>
|
|
||||||
<div className='sticky top-0 pt-5 pb-3 bg-background-default-subtle z-10'>
|
|
||||||
<div className='title-2xl-semi-bold bg-gradient-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text text-transparent'>More from Marketplace</div>
|
|
||||||
<div className='flex items-center text-center body-md-regular text-text-tertiary'>
|
|
||||||
Discover
|
|
||||||
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
|
||||||
models
|
|
||||||
</span>
|
|
||||||
,
|
|
||||||
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
|
||||||
tools
|
|
||||||
</span>
|
|
||||||
,
|
|
||||||
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
|
||||||
extensions
|
|
||||||
</span>
|
|
||||||
and
|
|
||||||
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
|
||||||
bundles
|
|
||||||
</span>
|
|
||||||
in Dify Marketplace
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='py-3'>
|
|
||||||
<div className='title-xl-semi-bold text-text-primary'>Featured</div>
|
|
||||||
<div className='system-xs-regular text-text-tertiary'>Our top picks to get you started</div>
|
|
||||||
<div className='grid grid-cols-4 gap-3 mt-2'>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='py-3'>
|
|
||||||
<div className='title-xl-semi-bold text-text-primary'>Popular</div>
|
|
||||||
<div className='system-xs-regular text-text-tertiary'>Explore the library and discover the incredible work of our community</div>
|
|
||||||
<div className='grid grid-cols-4 gap-3 mt-2'>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
payload={toolNotion as any}
|
|
||||||
footer={
|
|
||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Marketplace
|
|
||||||
35
web/app/components/tools/marketplace/hooks.ts
Normal file
35
web/app/components/tools/marketplace/hooks.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import type { Plugin } from '@/app/components/plugins/types'
|
||||||
|
import type { MarketplaceCollection } from '@/app/components/plugins/marketplace/types'
|
||||||
|
|
||||||
|
export const useMarketplace = () => {
|
||||||
|
const [marketplaceCollections, setMarketplaceCollections] = useState<MarketplaceCollection[]>([])
|
||||||
|
const [marketplaceCollectionPluginsMap, setMarketplaceCollectionPluginsMap] = useState<Record<string, Plugin[]>>({})
|
||||||
|
const getMarketplaceCollections = useCallback(async () => {
|
||||||
|
const marketplaceCollectionsData = await globalThis.fetch('https://marketplace.dify.dev/api/v1/collections')
|
||||||
|
const marketplaceCollectionsDataJson = await marketplaceCollectionsData.json()
|
||||||
|
const marketplaceCollections = marketplaceCollectionsDataJson.data.collections
|
||||||
|
const marketplaceCollectionPluginsMap = {} as Record<string, Plugin[]>
|
||||||
|
await Promise.all(marketplaceCollections.map(async (collection: MarketplaceCollection) => {
|
||||||
|
const marketplaceCollectionPluginsData = await globalThis.fetch(`https://marketplace.dify.dev/api/v1/collections/${collection.name}/plugins`)
|
||||||
|
const marketplaceCollectionPluginsDataJson = await marketplaceCollectionPluginsData.json()
|
||||||
|
const plugins = marketplaceCollectionPluginsDataJson.data.plugins
|
||||||
|
|
||||||
|
marketplaceCollectionPluginsMap[collection.name] = plugins
|
||||||
|
}))
|
||||||
|
setMarketplaceCollections(marketplaceCollections)
|
||||||
|
setMarketplaceCollectionPluginsMap(marketplaceCollectionPluginsMap)
|
||||||
|
}, [])
|
||||||
|
useEffect(() => {
|
||||||
|
getMarketplaceCollections()
|
||||||
|
}, [getMarketplaceCollections])
|
||||||
|
|
||||||
|
return {
|
||||||
|
marketplaceCollections,
|
||||||
|
marketplaceCollectionPluginsMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
48
web/app/components/tools/marketplace/index.tsx
Normal file
48
web/app/components/tools/marketplace/index.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { RiArrowUpDoubleLine } from '@remixicon/react'
|
||||||
|
import { useMarketplace } from './hooks'
|
||||||
|
import List from '@/app/components/plugins/marketplace/list'
|
||||||
|
|
||||||
|
type MarketplaceProps = {
|
||||||
|
onMarketplaceScroll: () => void
|
||||||
|
}
|
||||||
|
const Marketplace = ({
|
||||||
|
onMarketplaceScroll,
|
||||||
|
}: MarketplaceProps) => {
|
||||||
|
const { marketplaceCollections, marketplaceCollectionPluginsMap } = useMarketplace()
|
||||||
|
return (
|
||||||
|
<div className='shrink-0 sticky -bottom-[442px] h-[530px] overflow-y-auto px-12 py-2 pt-0 bg-background-default-subtle'>
|
||||||
|
<RiArrowUpDoubleLine
|
||||||
|
className='absolute top-2 left-1/2 -translate-x-1/2 w-4 h-4 text-text-quaternary cursor-pointer'
|
||||||
|
onClick={() => onMarketplaceScroll()}
|
||||||
|
/>
|
||||||
|
<div className='sticky top-0 pt-5 pb-3 bg-background-default-subtle z-10'>
|
||||||
|
<div className='title-2xl-semi-bold bg-gradient-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text text-transparent'>More from Marketplace</div>
|
||||||
|
<div className='flex items-center text-center body-md-regular text-text-tertiary'>
|
||||||
|
Discover
|
||||||
|
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
||||||
|
models
|
||||||
|
</span>
|
||||||
|
,
|
||||||
|
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
||||||
|
tools
|
||||||
|
</span>
|
||||||
|
,
|
||||||
|
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
||||||
|
extensions
|
||||||
|
</span>
|
||||||
|
and
|
||||||
|
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
||||||
|
bundles
|
||||||
|
</span>
|
||||||
|
in Dify Marketplace
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<List
|
||||||
|
marketplaceCollections={marketplaceCollections}
|
||||||
|
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Marketplace
|
||||||
Loading…
x
Reference in New Issue
Block a user