mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-14 03:15:54 +08:00
marketplace
This commit is contained in:
parent
684896d100
commit
39a6f0943d
45
web/app/components/plugins/marketplace/context.tsx
Normal file
45
web/app/components/plugins/marketplace/context.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useContextSelector,
|
||||
} from 'use-context-selector'
|
||||
|
||||
export type MarketplaceContextValue = {
|
||||
intersected: boolean
|
||||
setIntersected: (intersected: boolean) => void
|
||||
}
|
||||
|
||||
export const MarketplaceContext = createContext<MarketplaceContextValue>({
|
||||
intersected: true,
|
||||
setIntersected: () => {},
|
||||
})
|
||||
|
||||
type MarketplaceContextProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function useMarketplaceContext(selector: (value: MarketplaceContextValue) => any) {
|
||||
return useContextSelector(MarketplaceContext, selector)
|
||||
}
|
||||
|
||||
export const MarketplaceContextProvider = ({
|
||||
children,
|
||||
}: MarketplaceContextProviderProps) => {
|
||||
const [intersected, setIntersected] = useState(true)
|
||||
|
||||
return (
|
||||
<MarketplaceContext.Provider
|
||||
value={{
|
||||
intersected,
|
||||
setIntersected,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MarketplaceContext.Provider>
|
||||
)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import IntersectionLine from '../intersection-line'
|
||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
||||
|
||||
type DescriptionWrapperProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
const DescriptionWrapper = ({
|
||||
children,
|
||||
}: DescriptionWrapperProps) => {
|
||||
const containerRef = usePluginPageContext(v => v.containerRef)
|
||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
||||
const setScrollDisabled = usePluginPageContext(v => v.setScrollDisabled)
|
||||
|
||||
const handleScrollIntersectionChange = useCallback((isIntersecting: boolean) => {
|
||||
if (!isIntersecting && !scrollDisabled) {
|
||||
setScrollDisabled(true)
|
||||
setTimeout(() => {
|
||||
if (containerRef && containerRef.current)
|
||||
containerRef.current.scrollTop = 0
|
||||
}, 100)
|
||||
}
|
||||
}, [containerRef, scrollDisabled, setScrollDisabled])
|
||||
|
||||
return !scrollDisabled && (
|
||||
<>
|
||||
{children}
|
||||
<IntersectionLine
|
||||
containerRef={containerRef}
|
||||
intersectedCallback={handleScrollIntersectionChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DescriptionWrapper
|
@ -1,20 +0,0 @@
|
||||
import Description from '../description'
|
||||
import DescriptionWrapper from '../description/wrapper'
|
||||
import SearchBoxWrapper from '../search-box/wrapper'
|
||||
import PluginTypeSwitch from '../plugin-type-switch'
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<>
|
||||
<DescriptionWrapper>
|
||||
<Description />
|
||||
</DescriptionWrapper>
|
||||
<div className='flex items-center justify-center mt-[15px] mb-4'>
|
||||
<SearchBoxWrapper />
|
||||
</div>
|
||||
<PluginTypeSwitch />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
@ -1,27 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type HeaderWrapperProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
const HeaderWrapper = ({
|
||||
children,
|
||||
}: HeaderWrapperProps) => {
|
||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'py-10',
|
||||
scrollDisabled && 'absolute left-1/2 -translate-x-1/2 -top-[100px] pb-3',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderWrapper
|
@ -1,18 +1,19 @@
|
||||
import Header from './header'
|
||||
import HeaderWrapper from './header/wrapper'
|
||||
import { MarketplaceContextProvider } from './context'
|
||||
import Description from './description'
|
||||
import IntersectionLine from './intersection-line'
|
||||
import SearchBox from './search-box'
|
||||
import PluginTypeSwitch from './plugin-type-switch'
|
||||
import List from './list'
|
||||
import ListWrapper from './list/wrapper'
|
||||
|
||||
const Marketplace = () => {
|
||||
return (
|
||||
<div className='grow relative flex flex-col w-full h-0'>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
<ListWrapper>
|
||||
<List />
|
||||
</ListWrapper>
|
||||
</div>
|
||||
<MarketplaceContextProvider>
|
||||
<Description />
|
||||
<IntersectionLine />
|
||||
<SearchBox />
|
||||
<PluginTypeSwitch />
|
||||
<List />
|
||||
</MarketplaceContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,21 +1,30 @@
|
||||
import { useEffect } from 'react'
|
||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
||||
import { useMarketplaceContext } from '@/app/components/plugins/marketplace/context'
|
||||
|
||||
export const useScrollIntersection = (
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
anchorRef: React.RefObject<HTMLDivElement>,
|
||||
callback: (isIntersecting: boolean) => void,
|
||||
) => {
|
||||
const containerRef = usePluginPageContext(v => v.containerRef)
|
||||
const intersected = useMarketplaceContext(v => v.intersected)
|
||||
const setIntersected = useMarketplaceContext(v => v.setIntersected)
|
||||
|
||||
useEffect(() => {
|
||||
let observer: IntersectionObserver | undefined
|
||||
if (containerRef?.current && anchorRef.current) {
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
const isIntersecting = entries[0].isIntersecting
|
||||
callback(isIntersecting)
|
||||
|
||||
if (isIntersecting && !intersected)
|
||||
setIntersected(true)
|
||||
|
||||
if (!isIntersecting && intersected)
|
||||
setIntersected(false)
|
||||
}, {
|
||||
root: containerRef.current,
|
||||
})
|
||||
observer.observe(anchorRef.current)
|
||||
}
|
||||
return () => observer?.disconnect()
|
||||
}, [containerRef, anchorRef, callback])
|
||||
}, [containerRef, anchorRef, intersected, setIntersected])
|
||||
}
|
||||
|
@ -3,24 +3,13 @@
|
||||
import { useRef } from 'react'
|
||||
import { useScrollIntersection } from './hooks'
|
||||
|
||||
type IntersectionLineProps = {
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
intersectedCallback: (isIntersecting: boolean) => void
|
||||
}
|
||||
const IntersectionLine = ({
|
||||
containerRef,
|
||||
intersectedCallback,
|
||||
}: IntersectionLineProps) => {
|
||||
const IntersectionLine = () => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useScrollIntersection(
|
||||
containerRef,
|
||||
ref,
|
||||
intersectedCallback,
|
||||
)
|
||||
useScrollIntersection(ref)
|
||||
|
||||
return (
|
||||
<div ref={ref} className='h-[1px] bg-transparent'></div>
|
||||
<div ref={ref} className='mb-4 h-[1px] bg-transparent'></div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ const List = () => {
|
||||
const locale = getLocaleOnServer()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='px-12 py-2 bg-background-default-subtle'>
|
||||
<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>
|
||||
@ -223,7 +223,7 @@ const List = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,39 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ListWrapperProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
const ListWrapper = ({
|
||||
children,
|
||||
}: ListWrapperProps) => {
|
||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
||||
const setScrollDisabled = usePluginPageContext(v => v.setScrollDisabled)
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
scrollDisabled && (
|
||||
<div className='h-[60px]'></div>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className={cn(
|
||||
'px-12 py-2 bg-background-default-subtle',
|
||||
scrollDisabled && 'grow h-0 overflow-y-auto',
|
||||
)}
|
||||
onScroll={(e) => {
|
||||
if ((e.target as HTMLElement).scrollTop <= 0)
|
||||
setScrollDisabled(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListWrapper
|
@ -43,7 +43,9 @@ const PluginTypeSwitch = ({
|
||||
const [activeType, setActiveType] = useState('all')
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-center space-x-2'>
|
||||
<div className={cn(
|
||||
'sticky top-[60px] flex items-center justify-center py-3 bg-background-body space-x-2 z-10',
|
||||
)}>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
|
@ -5,18 +5,18 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useMarketplaceContext } from '../context'
|
||||
import TagsFilter from './tags-filter'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type SearchBoxProps = {
|
||||
onChange?: (searchText: string, tags: string[]) => void
|
||||
widthShouldChange?: boolean
|
||||
}
|
||||
const SearchBox = ({
|
||||
onChange,
|
||||
widthShouldChange,
|
||||
}: SearchBoxProps) => {
|
||||
const intersected = useMarketplaceContext(v => v.intersected)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
||||
|
||||
@ -28,8 +28,8 @@ const SearchBox = ({
|
||||
return (
|
||||
<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',
|
||||
widthShouldChange && 'w-[508px] transition-[width] duration-300',
|
||||
'sticky top-3 flex items-center m-auto p-1.5 w-[640px] h-11 border border-components-chat-input-border bg-components-panel-bg-blur rounded-xl shadow-md z-[11]',
|
||||
!intersected && 'w-[508px] transition-[width] duration-300',
|
||||
)}
|
||||
>
|
||||
<TagsFilter
|
||||
|
@ -1,14 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import SearchBox from '.'
|
||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
||||
|
||||
const Wrapper = () => {
|
||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
||||
|
||||
return (
|
||||
<SearchBox widthShouldChange={scrollDisabled} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Wrapper
|
@ -3,7 +3,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
createContext,
|
||||
@ -12,14 +11,10 @@ import {
|
||||
|
||||
export type PluginPageContextValue = {
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
scrollDisabled: boolean
|
||||
setScrollDisabled: (scrollDisabled: boolean) => void
|
||||
}
|
||||
|
||||
export const PluginPageContext = createContext<PluginPageContextValue>({
|
||||
containerRef: { current: null },
|
||||
scrollDisabled: false,
|
||||
setScrollDisabled: () => {},
|
||||
})
|
||||
|
||||
type PluginPageContextProviderProps = {
|
||||
@ -34,14 +29,11 @@ export const PluginPageContextProvider = ({
|
||||
children,
|
||||
}: PluginPageContextProviderProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [scrollDisabled, setScrollDisabled] = useState(false)
|
||||
|
||||
return (
|
||||
<PluginPageContext.Provider
|
||||
value={{
|
||||
containerRef,
|
||||
scrollDisabled,
|
||||
setScrollDisabled,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -32,7 +32,6 @@ const PluginPage = ({
|
||||
const { t } = useTranslation()
|
||||
const { setShowPluginSettingModal } = useModalContext() as any
|
||||
const containerRef = usePluginPageContext(v => v.containerRef)
|
||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
@ -51,12 +50,11 @@ const PluginPage = ({
|
||||
className={cn('grow relative flex flex-col overflow-y-auto border-t border-divider-subtle', activeTab === 'plugins'
|
||||
? 'rounded-t-xl bg-components-panel-bg'
|
||||
: 'bg-background-body',
|
||||
activeTab === 'discover' && scrollDisabled && 'overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'sticky top-0 flex min-h-[60px] px-12 pt-4 pb-2 items-center self-stretch gap-1',
|
||||
'sticky top-0 flex min-h-[60px] px-12 pt-4 pb-2 items-center self-stretch gap-1 bg-background-body z-10',
|
||||
)}
|
||||
>
|
||||
<div className='flex justify-between items-center w-full'>
|
||||
|
Loading…
x
Reference in New Issue
Block a user