feat: introduce creation type management in marketplace components for improved data handling

This commit is contained in:
yessenia 2026-02-10 21:42:42 +08:00
parent 9f8289b185
commit 5c6da34539
7 changed files with 70 additions and 87 deletions

View File

@ -1,10 +1,10 @@
import type { SearchTab } from './search-params'
import type { PluginsSort, SearchParamsFromCollection } from './types'
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useQueryState } from 'nuqs'
import { useCallback } from 'react'
import { DEFAULT_SORT, getValidatedPluginCategory, getValidatedTemplateCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants'
import type { SearchTab } from './search-params'
import { marketplaceSearchParamsParsers } from './search-params'
import { CREATION_TYPE, marketplaceSearchParamsParsers } from './search-params'
const marketplaceSortAtom = atom<PluginsSort>(DEFAULT_SORT)
export function useMarketplaceSort() {
@ -37,6 +37,10 @@ export function useSearchTab() {
return useQueryState('searchTab', marketplaceSearchParamsParsers.searchTab)
}
export function useCreationType() {
return useQueryState('creationType', marketplaceSearchParamsParsers.creationType)
}
/**
* Not all categories have collections, so we need to
* force the search mode for those categories.
@ -44,14 +48,17 @@ export function useSearchTab() {
export const searchModeAtom = atom<true | null>(null)
export function useMarketplaceSearchMode() {
// const [searchText] = useSearchText()
const [creationType] = useCreationType()
const [searchText] = useSearchText()
const [searchTab] = useSearchTab()
// const [filterPluginTags] = useFilterPluginTags()
const [filterPluginTags] = useFilterPluginTags()
const [activePluginCategory] = useActivePluginCategory()
const isPluginsView = creationType === CREATION_TYPE.plugins
const searchMode = useAtomValue(searchModeAtom)
const isSearchMode = searchTab
|| (searchMode ?? (!PLUGIN_CATEGORY_WITH_COLLECTIONS.has(activePluginCategory)))
const isSearchMode = searchTab || searchText
|| (isPluginsView && filterPluginTags.length > 0)
|| (searchMode ?? (isPluginsView && !PLUGIN_CATEGORY_WITH_COLLECTIONS.has(activePluginCategory)))
return isSearchMode
}

View File

@ -7,8 +7,9 @@ import { useEffect, useLayoutEffect, useRef } from 'react'
import marketPlaceBg from '@/public/marketplace/hero-bg.jpg'
import marketplaceGradientNoise from '@/public/marketplace/hero-gradient-noise.svg'
import { cn } from '@/utils/classnames'
import { useCreationType } from '../atoms'
import { PluginCategorySwitch, TemplateCategorySwitch } from '../category-switch/index'
import { useMarketplaceData } from '../state'
import { CREATION_TYPE } from '../search-params'
type DescriptionProps = {
className?: string
@ -29,8 +30,8 @@ export const Description = ({
marketplaceNav,
}: DescriptionProps) => {
const { t } = useTranslation('plugin')
const { creationType } = useMarketplaceData()
const isTemplatesView = creationType === 'templates'
const [creationType] = useCreationType()
const isTemplatesView = creationType === CREATION_TYPE.templates
const heroTitleKey = isTemplatesView ? 'marketplace.templatesHeroTitle' : 'marketplace.pluginsHeroTitle'
const heroSubtitleKey = isTemplatesView ? 'marketplace.templatesHeroSubtitle' : 'marketplace.pluginsHeroSubtitle'
const rafRef = useRef<number | null>(null)

View File

@ -4,7 +4,7 @@ import { createLoader } from 'nuqs/server'
import { getQueryClientServer } from '@/context/query-client-server'
import { marketplaceQuery } from '@/service/client'
import { getValidatedPluginCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants'
import { marketplaceSearchParamsParsers } from './search-params'
import { CREATION_TYPE, marketplaceSearchParamsParsers } from './search-params'
import { getCollectionsParams, getMarketplaceCollectionsAndPlugins, getMarketplaceTemplateCollectionsAndTemplates } from './utils'
// The server side logic should move to marketplace's codebase so that we can get rid of Next.js
@ -17,7 +17,7 @@ async function getDehydratedState(searchParams?: Promise<SearchParams>) {
const params = await loadSearchParams(searchParams)
const queryClient = getQueryClientServer()
if (params.creationType === 'templates') {
if (params.creationType === CREATION_TYPE.templates) {
await queryClient.prefetchQuery({
queryKey: marketplaceQuery.templateCollections.list.queryKey({ input: { query: undefined } }),
queryFn: () => getMarketplaceTemplateCollectionsAndTemplates(),

View File

@ -1,7 +1,7 @@
'use client'
import Loading from '@/app/components/base/loading'
import { useMarketplaceData } from '../state'
import { isPluginsData, useMarketplaceData } from '../state'
import FlatList from './flat-list'
import ListWithCollection from './list-with-collection'
@ -11,50 +11,33 @@ type ListWrapperProps = {
const ListWrapper = ({ showInstallButton }: ListWrapperProps) => {
const marketplaceData = useMarketplaceData()
const { creationType, isLoading, page, isFetchingNextPage } = marketplaceData
const isPluginView = creationType === 'plugins'
const { isLoading, page, isFetchingNextPage } = marketplaceData
const renderContent = () => {
if (!isPluginView) {
const { templateCollections, templateCollectionTemplatesMap, templates } = marketplaceData
if (templates !== undefined) {
return (
<FlatList
if (isPluginsData(marketplaceData)) {
const { pluginCollections, pluginCollectionPluginsMap, plugins } = marketplaceData
return plugins !== undefined
? <FlatList variant="plugins" items={plugins} showInstallButton={showInstallButton} />
: (
<ListWithCollection
variant="plugins"
collections={pluginCollections || []}
collectionItemsMap={pluginCollectionPluginsMap || {}}
showInstallButton={showInstallButton}
/>
)
}
const { templateCollections, templateCollectionTemplatesMap, templates } = marketplaceData
return templates !== undefined
? <FlatList variant="templates" items={templates} />
: (
<ListWithCollection
variant="templates"
items={templates}
collections={templateCollections || []}
collectionItemsMap={templateCollectionTemplatesMap || {}}
/>
)
}
return (
<ListWithCollection
variant="templates"
collections={templateCollections || []}
collectionItemsMap={templateCollectionTemplatesMap || {}}
/>
)
}
const { pluginCollections, pluginCollectionPluginsMap, plugins } = marketplaceData
if (plugins !== undefined) {
return (
<FlatList
variant="plugins"
items={plugins}
showInstallButton={showInstallButton}
/>
)
}
return (
<ListWithCollection
variant="plugins"
collections={pluginCollections || []}
collectionItemsMap={pluginCollectionPluginsMap || {}}
showInstallButton={showInstallButton}
/>
)
}
return (

View File

@ -1,12 +1,17 @@
import { parseAsArrayOf, parseAsString, parseAsStringEnum } from 'nuqs/server'
export type CreationType = 'plugins' | 'templates'
export const CREATION_TYPE = {
plugins: 'plugins',
templates: 'templates',
} as const
export type CreationType = typeof CREATION_TYPE[keyof typeof CREATION_TYPE]
export const marketplaceSearchParamsParsers = {
category: parseAsString.withDefault('all').withOptions({ history: 'replace', clearOnDefault: false }),
q: parseAsString.withDefault('').withOptions({ history: 'replace' }),
tags: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
creationType: parseAsStringEnum<CreationType>(['plugins', 'templates']).withDefault('plugins').withOptions({ history: 'replace' }),
creationType: parseAsStringEnum<CreationType>([CREATION_TYPE.plugins, CREATION_TYPE.templates]).withDefault(CREATION_TYPE.plugins).withOptions({ history: 'replace' }),
searchTab: parseAsStringEnum<SearchTab>(['all', 'plugins', 'templates', 'creators']).withDefault('').withOptions({ history: 'replace' }),
}

View File

@ -1,11 +1,11 @@
import type { PluginsSearchParams, TemplateSearchParams } from './types'
import { useDebounce } from 'ahooks'
import { useSearchParams } from 'next/navigation'
import { useCallback, useMemo } from 'react'
import { useActivePluginCategory, useActiveTemplateCategory, useFilterPluginTags, useMarketplaceSearchMode, useMarketplaceSortValue, useSearchText } from './atoms'
import { useActivePluginCategory, useActiveTemplateCategory, useCreationType, useFilterPluginTags, useMarketplaceSearchMode, useMarketplaceSortValue, useSearchText } from './atoms'
import { CATEGORY_ALL } from './constants'
import { useMarketplaceContainerScroll } from './hooks'
import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins, useMarketplaceTemplateCollectionsAndTemplates, useMarketplaceTemplates } from './query'
import { CREATION_TYPE } from './search-params'
import { getCollectionsParams, getPluginFilterType, mapTemplateDetailToTemplate } from './utils'
const getCategory = (category: string) => {
@ -121,31 +121,22 @@ export function useTemplatesMarketplaceData(enabled = true) {
}
}
type PluginsMarketplaceData = ReturnType<typeof usePluginsMarketplaceData>
type TemplatesMarketplaceData = ReturnType<typeof useTemplatesMarketplaceData>
type MarketplaceData
= ({ creationType: 'plugins' } & PluginsMarketplaceData)
| ({ creationType: 'templates' } & TemplatesMarketplaceData)
export type PluginsMarketplaceData = ReturnType<typeof usePluginsMarketplaceData>
export type TemplatesMarketplaceData = ReturnType<typeof useTemplatesMarketplaceData>
export type MarketplaceData = PluginsMarketplaceData | TemplatesMarketplaceData
export function isPluginsData(data: MarketplaceData): data is PluginsMarketplaceData {
return 'pluginCollections' in data
}
/**
* Main hook that routes to appropriate data based on creationType
* Returns either plugins or templates data based on URL parameter
*/
export function useMarketplaceData(): MarketplaceData {
const searchParams = useSearchParams()
const creationType = (searchParams.get('creationType') || 'plugins') as 'plugins' | 'templates'
const [creationType] = useCreationType()
const pluginsData = usePluginsMarketplaceData(creationType === 'plugins')
const templatesData = useTemplatesMarketplaceData(creationType === 'templates')
if (creationType === 'templates') {
return {
creationType,
...templatesData,
}
}
return {
creationType,
...pluginsData,
}
const pluginsData = usePluginsMarketplaceData(creationType === CREATION_TYPE.plugins)
const templatesData = useTemplatesMarketplaceData(creationType === CREATION_TYPE.templates)
return creationType === CREATION_TYPE.templates ? templatesData : pluginsData
}

View File

@ -1,7 +1,6 @@
'use client'
import { RiAddLine, RiBookOpenLine, RiGithubLine } from '@remixicon/react'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
@ -12,8 +11,10 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { CREATION_TYPE } from '@/app/components/plugins/marketplace/search-params'
import { useDocLink } from '@/context/i18n'
import { cn } from '@/utils/classnames'
import { useCreationType } from '../marketplace/atoms'
type DropdownItemProps = {
href: string
@ -80,23 +81,18 @@ export const SubmitRequestDropdown = () => {
)
}
type CreationTypeTabsProps = {
creationType?: string
}
export const CreationTypeTabs = ({ creationType: creationTypeProp }: CreationTypeTabsProps = {}) => {
export const CreationTypeTabs = () => {
const { t } = useTranslation()
const searchParams = useSearchParams()
const creationType = creationTypeProp || searchParams.get('creationType') || 'plugins'
const [creationType] = useCreationType()
return (
<div className="flex items-center gap-1">
<Link
href="/?creationType=plugins"
href={`/?creationType=${CREATION_TYPE.plugins}`}
className={cn(
buttonVariants({ variant: 'ghost' }),
'flex items-center gap-1 px-3 py-2 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
creationType === 'plugins' && 'bg-state-base-hover text-text-secondary',
creationType === CREATION_TYPE.plugins && 'bg-state-base-hover text-text-secondary',
)}
>
<Plugin className="h-4 w-4 shrink-0" />
@ -105,11 +101,11 @@ export const CreationTypeTabs = ({ creationType: creationTypeProp }: CreationTyp
</span>
</Link>
<Link
href="/?creationType=templates"
href={`/?creationType=${CREATION_TYPE.templates}`}
className={cn(
buttonVariants({ variant: 'ghost' }),
'flex items-center gap-1 px-3 py-2 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
creationType === 'templates' && 'bg-state-base-hover text-text-secondary',
creationType === CREATION_TYPE.templates && 'bg-state-base-hover text-text-secondary',
)}
>
<Playground className="h-4 w-4 shrink-0" />