marketplace

This commit is contained in:
StyleZhang 2024-10-12 12:46:29 +08:00
parent 060a894bd1
commit c1e0a939b0
10 changed files with 321 additions and 54 deletions

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

View 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

View 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

View File

@ -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])
}

View File

@ -1,39 +1,20 @@
import SearchBox from './search-box'
import PluginTypeSwitch from './plugin-type-switch'
import { MarketplaceContextProvider } from './context'
import HeaderWrapper from './header-wrapper'
import Header from './header'
import ListWrapper from './list-wrapper'
import List from './list'
const Marketplace = () => {
return (
<div className='w-full'>
<div className='py-10'>
<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>
<div className='flex items-center justify-center mb-4'>
<SearchBox />
</div>
<PluginTypeSwitch />
</div>
<List />
<MarketplaceContextProvider>
<HeaderWrapper>
<Header />
</HeaderWrapper>
<ListWrapper>
<List />
</ListWrapper>
</MarketplaceContextProvider>
</div>
)
}

View File

@ -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])
}

View File

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

View 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

View File

@ -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>
@ -95,9 +95,135 @@ const List = () => {
<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>
</>
)
}

View File

@ -5,8 +5,10 @@ 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
@ -16,6 +18,7 @@ const SearchBox = ({
}: SearchBoxProps) => {
const [searchText, setSearchText] = useState('')
const [selectedTags, setSelectedTags] = useState<string[]>([])
const scrollIntersected = useMarketplaceContext(v => v.scrollIntersected)
const handleTagsChange = useCallback((tags: string[]) => {
setSelectedTags(tags)
@ -23,7 +26,12 @@ const SearchBox = ({
}, [searchText, onChange])
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
value={selectedTags}
onChange={handleTagsChange}