mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-16 20:15:58 +08:00
marketplace
This commit is contained in:
parent
060a894bd1
commit
c1e0a939b0
41
web/app/components/plugins/marketplace/context.tsx
Normal file
41
web/app/components/plugins/marketplace/context.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContextSelector,
|
||||||
|
} from 'use-context-selector'
|
||||||
|
|
||||||
|
export type MarketplaceContextValue = {
|
||||||
|
scrollIntersected: boolean
|
||||||
|
setScrollIntersected: (scrollIntersected: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarketplaceContext = createContext<MarketplaceContextValue>({
|
||||||
|
scrollIntersected: false,
|
||||||
|
setScrollIntersected: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
type MarketplaceContextProviderProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMarketplaceContext(selector: (value: MarketplaceContextValue) => any) {
|
||||||
|
return useContextSelector(MarketplaceContext, selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarketplaceContextProvider = ({
|
||||||
|
children,
|
||||||
|
}: MarketplaceContextProviderProps) => {
|
||||||
|
const [scrollIntersected, setScrollIntersected] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarketplaceContext.Provider value={{
|
||||||
|
scrollIntersected,
|
||||||
|
setScrollIntersected,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</MarketplaceContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
23
web/app/components/plugins/marketplace/header-wrapper.tsx
Normal file
23
web/app/components/plugins/marketplace/header-wrapper.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type HeaderWrapperProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
const HeaderWrapper = ({
|
||||||
|
children,
|
||||||
|
}: HeaderWrapperProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'py-10',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderWrapper
|
39
web/app/components/plugins/marketplace/header.tsx
Normal file
39
web/app/components/plugins/marketplace/header.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import SearchBox from './search-box'
|
||||||
|
import PluginTypeSwitch from './plugin-type-switch'
|
||||||
|
import IntersectionLine from './intersection-line'
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className='mb-2 text-center title-4xl-semi-bold text-text-primary'>
|
||||||
|
Empower your AI development
|
||||||
|
</h1>
|
||||||
|
<h2 className='flex justify-center items-center mb-4 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
|
||||||
|
</h2>
|
||||||
|
<IntersectionLine />
|
||||||
|
<div className='flex items-center justify-center mb-[15px]'>
|
||||||
|
<SearchBox />
|
||||||
|
</div>
|
||||||
|
<PluginTypeSwitch />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header
|
@ -1,20 +0,0 @@
|
|||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
export const useScrollIntersection = (
|
|
||||||
rootRef: React.RefObject<HTMLDivElement>,
|
|
||||||
anchorRef: React.RefObject<HTMLDivElement>,
|
|
||||||
callback: (isIntersecting: boolean) => void,
|
|
||||||
) => {
|
|
||||||
useEffect(() => {
|
|
||||||
let observer: IntersectionObserver | undefined
|
|
||||||
if (rootRef.current && anchorRef.current) {
|
|
||||||
observer = new IntersectionObserver((entries) => {
|
|
||||||
callback(entries[0].isIntersecting)
|
|
||||||
}, {
|
|
||||||
root: rootRef.current,
|
|
||||||
})
|
|
||||||
observer.observe(anchorRef.current)
|
|
||||||
}
|
|
||||||
return () => observer?.disconnect()
|
|
||||||
}, [rootRef, anchorRef, callback])
|
|
||||||
}
|
|
@ -1,39 +1,20 @@
|
|||||||
import SearchBox from './search-box'
|
import { MarketplaceContextProvider } from './context'
|
||||||
import PluginTypeSwitch from './plugin-type-switch'
|
import HeaderWrapper from './header-wrapper'
|
||||||
|
import Header from './header'
|
||||||
|
import ListWrapper from './list-wrapper'
|
||||||
import List from './list'
|
import List from './list'
|
||||||
|
|
||||||
const Marketplace = () => {
|
const Marketplace = () => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div className='py-10'>
|
<MarketplaceContextProvider>
|
||||||
<h1 className='mb-2 text-center title-4xl-semi-bold text-text-primary'>
|
<HeaderWrapper>
|
||||||
Empower your AI development
|
<Header />
|
||||||
</h1>
|
</HeaderWrapper>
|
||||||
<h2 className='flex justify-center items-center mb-4 text-center body-md-regular text-text-tertiary'>
|
<ListWrapper>
|
||||||
Discover
|
<List />
|
||||||
<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">
|
</ListWrapper>
|
||||||
models
|
</MarketplaceContextProvider>
|
||||||
</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
|
|
||||||
</h2>
|
|
||||||
<div className='flex items-center justify-center mb-4'>
|
|
||||||
<SearchBox />
|
|
||||||
</div>
|
|
||||||
<PluginTypeSwitch />
|
|
||||||
</div>
|
|
||||||
<List />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useContextSelector } from 'use-context-selector'
|
||||||
|
import { PluginPageContext } from '../../plugin-page/context'
|
||||||
|
import { MarketplaceContext } from '../context'
|
||||||
|
|
||||||
|
export const useScrollIntersection = (
|
||||||
|
anchorRef: React.RefObject<HTMLDivElement>,
|
||||||
|
) => {
|
||||||
|
const containerRef = useContextSelector(PluginPageContext, v => v.containerRef)
|
||||||
|
const scrollIntersected = useContextSelector(MarketplaceContext, v => v.scrollIntersected)
|
||||||
|
const setScrollIntersected = useContextSelector(MarketplaceContext, v => v.setScrollIntersected)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let observer: IntersectionObserver | undefined
|
||||||
|
if (containerRef.current && anchorRef.current) {
|
||||||
|
observer = new IntersectionObserver((entries) => {
|
||||||
|
console.log(entries, 'entries')
|
||||||
|
if (entries[0].isIntersecting && !scrollIntersected)
|
||||||
|
setScrollIntersected(true)
|
||||||
|
|
||||||
|
if (!entries[0].isIntersecting && scrollIntersected)
|
||||||
|
setScrollIntersected(false)
|
||||||
|
}, {
|
||||||
|
root: containerRef.current,
|
||||||
|
})
|
||||||
|
observer.observe(anchorRef.current)
|
||||||
|
}
|
||||||
|
return () => observer?.disconnect()
|
||||||
|
}, [containerRef, anchorRef, scrollIntersected, setScrollIntersected])
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { useScrollIntersection } from './hooks'
|
||||||
|
|
||||||
|
const IntersectionLine = () => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useScrollIntersection(ref)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className='h-[1px] bg-transparent'></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IntersectionLine
|
23
web/app/components/plugins/marketplace/list-wrapper.tsx
Normal file
23
web/app/components/plugins/marketplace/list-wrapper.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type ListWrapperProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
const ListWrapper = ({
|
||||||
|
children,
|
||||||
|
}: ListWrapperProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'px-12 py-2 bg-background-default-subtle',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListWrapper
|
@ -7,7 +7,7 @@ const List = () => {
|
|||||||
const locale = getLocaleOnServer()
|
const locale = getLocaleOnServer()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='px-12 py-2 bg-background-default-subtle'>
|
<>
|
||||||
<div className='py-3'>
|
<div className='py-3'>
|
||||||
<div className='title-xl-semi-bold text-text-primary'>Featured</div>
|
<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='system-xs-regular text-text-tertiary'>Our top picks to get you started</div>
|
||||||
@ -95,9 +95,135 @@ const List = () => {
|
|||||||
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
locale={locale}
|
||||||
|
payload={toolNotion as any}
|
||||||
|
footer={
|
||||||
|
<CardMoreInfo downloadCount={1234} tags={['Search', 'Productivity']} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,8 +5,10 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
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'
|
||||||
|
|
||||||
type SearchBoxProps = {
|
type SearchBoxProps = {
|
||||||
onChange?: (searchText: string, tags: string[]) => void
|
onChange?: (searchText: string, tags: string[]) => void
|
||||||
@ -16,6 +18,7 @@ const SearchBox = ({
|
|||||||
}: SearchBoxProps) => {
|
}: SearchBoxProps) => {
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
||||||
|
const scrollIntersected = useMarketplaceContext(v => v.scrollIntersected)
|
||||||
|
|
||||||
const handleTagsChange = useCallback((tags: string[]) => {
|
const handleTagsChange = useCallback((tags: string[]) => {
|
||||||
setSelectedTags(tags)
|
setSelectedTags(tags)
|
||||||
@ -23,7 +26,12 @@ const SearchBox = ({
|
|||||||
}, [searchText, onChange])
|
}, [searchText, onChange])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center p-1.5 w-[640px] h-11 border border-components-chat-input-border bg-components-panel-bg-blur rounded-xl shadow-md'>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center p-1.5 w-[640px] h-11 border border-components-chat-input-border bg-components-panel-bg-blur rounded-xl shadow-md',
|
||||||
|
!scrollIntersected && 'w-[508px] transition-[width] duration-300',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<TagsFilter
|
<TagsFilter
|
||||||
value={selectedTags}
|
value={selectedTags}
|
||||||
onChange={handleTagsChange}
|
onChange={handleTagsChange}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user