marketplace

This commit is contained in:
StyleZhang 2024-10-12 18:02:24 +08:00
parent 684896d100
commit 39a6f0943d
14 changed files with 83 additions and 186 deletions

View 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>
)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,18 +1,19 @@
import Header from './header' import { MarketplaceContextProvider } from './context'
import HeaderWrapper from './header/wrapper' 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 List from './list'
import ListWrapper from './list/wrapper'
const Marketplace = () => { const Marketplace = () => {
return ( return (
<div className='grow relative flex flex-col w-full h-0'> <MarketplaceContextProvider>
<HeaderWrapper> <Description />
<Header /> <IntersectionLine />
</HeaderWrapper> <SearchBox />
<ListWrapper> <PluginTypeSwitch />
<List /> <List />
</ListWrapper> </MarketplaceContextProvider>
</div>
) )
} }

View File

@ -1,21 +1,30 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import { useMarketplaceContext } from '@/app/components/plugins/marketplace/context'
export const useScrollIntersection = ( export const useScrollIntersection = (
containerRef: React.RefObject<HTMLDivElement>,
anchorRef: 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(() => { useEffect(() => {
let observer: IntersectionObserver | undefined let observer: IntersectionObserver | undefined
if (containerRef?.current && anchorRef.current) { if (containerRef?.current && anchorRef.current) {
observer = new IntersectionObserver((entries) => { observer = new IntersectionObserver((entries) => {
const isIntersecting = entries[0].isIntersecting const isIntersecting = entries[0].isIntersecting
callback(isIntersecting)
if (isIntersecting && !intersected)
setIntersected(true)
if (!isIntersecting && intersected)
setIntersected(false)
}, { }, {
root: containerRef.current, root: containerRef.current,
}) })
observer.observe(anchorRef.current) observer.observe(anchorRef.current)
} }
return () => observer?.disconnect() return () => observer?.disconnect()
}, [containerRef, anchorRef, callback]) }, [containerRef, anchorRef, intersected, setIntersected])
} }

View File

@ -3,24 +3,13 @@
import { useRef } from 'react' import { useRef } from 'react'
import { useScrollIntersection } from './hooks' import { useScrollIntersection } from './hooks'
type IntersectionLineProps = { const IntersectionLine = () => {
containerRef: React.RefObject<HTMLDivElement>
intersectedCallback: (isIntersecting: boolean) => void
}
const IntersectionLine = ({
containerRef,
intersectedCallback,
}: IntersectionLineProps) => {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
useScrollIntersection( useScrollIntersection(ref)
containerRef,
ref,
intersectedCallback,
)
return ( return (
<div ref={ref} className='h-[1px] bg-transparent'></div> <div ref={ref} className='mb-4 h-[1px] bg-transparent'></div>
) )
} }

View File

@ -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>
@ -223,7 +223,7 @@ const List = () => {
/> />
</div> </div>
</div> </div>
</> </div>
) )
} }

View File

@ -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

View File

@ -43,7 +43,9 @@ const PluginTypeSwitch = ({
const [activeType, setActiveType] = useState('all') const [activeType, setActiveType] = useState('all')
return ( 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 => ( options.map(option => (
<div <div

View File

@ -5,18 +5,18 @@ 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' import cn from '@/utils/classnames'
type SearchBoxProps = { type SearchBoxProps = {
onChange?: (searchText: string, tags: string[]) => void onChange?: (searchText: string, tags: string[]) => void
widthShouldChange?: boolean
} }
const SearchBox = ({ const SearchBox = ({
onChange, onChange,
widthShouldChange,
}: SearchBoxProps) => { }: SearchBoxProps) => {
const intersected = useMarketplaceContext(v => v.intersected)
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const [selectedTags, setSelectedTags] = useState<string[]>([]) const [selectedTags, setSelectedTags] = useState<string[]>([])
@ -28,8 +28,8 @@ const SearchBox = ({
return ( return (
<div <div
className={cn( 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', '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]',
widthShouldChange && 'w-[508px] transition-[width] duration-300', !intersected && 'w-[508px] transition-[width] duration-300',
)} )}
> >
<TagsFilter <TagsFilter

View File

@ -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

View File

@ -3,7 +3,6 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { import {
useRef, useRef,
useState,
} from 'react' } from 'react'
import { import {
createContext, createContext,
@ -12,14 +11,10 @@ import {
export type PluginPageContextValue = { export type PluginPageContextValue = {
containerRef: React.RefObject<HTMLDivElement> containerRef: React.RefObject<HTMLDivElement>
scrollDisabled: boolean
setScrollDisabled: (scrollDisabled: boolean) => void
} }
export const PluginPageContext = createContext<PluginPageContextValue>({ export const PluginPageContext = createContext<PluginPageContextValue>({
containerRef: { current: null }, containerRef: { current: null },
scrollDisabled: false,
setScrollDisabled: () => {},
}) })
type PluginPageContextProviderProps = { type PluginPageContextProviderProps = {
@ -34,14 +29,11 @@ export const PluginPageContextProvider = ({
children, children,
}: PluginPageContextProviderProps) => { }: PluginPageContextProviderProps) => {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const [scrollDisabled, setScrollDisabled] = useState(false)
return ( return (
<PluginPageContext.Provider <PluginPageContext.Provider
value={{ value={{
containerRef, containerRef,
scrollDisabled,
setScrollDisabled,
}} }}
> >
{children} {children}

View File

@ -32,7 +32,6 @@ const PluginPage = ({
const { t } = useTranslation() const { t } = useTranslation()
const { setShowPluginSettingModal } = useModalContext() as any const { setShowPluginSettingModal } = useModalContext() as any
const containerRef = usePluginPageContext(v => v.containerRef) const containerRef = usePluginPageContext(v => v.containerRef)
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
const options = useMemo(() => { const options = useMemo(() => {
return [ return [
@ -51,12 +50,11 @@ const PluginPage = ({
className={cn('grow relative flex flex-col overflow-y-auto border-t border-divider-subtle', activeTab === 'plugins' className={cn('grow relative flex flex-col overflow-y-auto border-t border-divider-subtle', activeTab === 'plugins'
? 'rounded-t-xl bg-components-panel-bg' ? 'rounded-t-xl bg-components-panel-bg'
: 'bg-background-body', : 'bg-background-body',
activeTab === 'discover' && scrollDisabled && 'overflow-hidden',
)} )}
> >
<div <div
className={cn( 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'> <div className='flex justify-between items-center w-full'>