feat: redesign marketplace homepage

This commit is contained in:
CodingOnStar 2026-07-23 18:57:28 +08:00 committed by CodingOnStar
parent e9eb130731
commit a866e2e2f3
41 changed files with 1190 additions and 22 deletions

View File

@ -156,6 +156,21 @@ export type TemplateDetailResponse = {
export type DownloadPluginResponse = Blob
const bannerListContract = base
.route({
path: '/banners',
method: 'GET',
})
.input(
type<{
query: {
page: 'plugins'
language: string
}
}>(),
)
.output(type<unknown>())
const collectionsContract = base
.route({
path: '/collections',
@ -229,6 +244,9 @@ const downloadPluginContract = base
.output(type<DownloadPluginResponse>())
export const marketplaceRouterContract = {
banners: {
list: bannerListContract,
},
collections: collectionsContract,
collectionPlugins: collectionPluginsContract,
searchAdvanced: searchAdvancedContract,

View File

@ -15,6 +15,7 @@ type CarouselProps = Readonly<{
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: 'horizontal' | 'vertical'
overlay?: React.ReactNode
}>
type CarouselContextValue = {
@ -49,7 +50,7 @@ type TCarousel = {
>
const Carousel: TCarousel = React.forwardRef(
({ orientation = 'horizontal', opts, plugins, className, children, ...props }, ref) => {
({ orientation = 'horizontal', opts, plugins, overlay, className, children, ...props }, ref) => {
const [carouselRef, api] = useEmblaCarousel(
{ ...opts, axis: orientation === 'horizontal' ? 'x' : 'y' },
plugins,
@ -98,6 +99,35 @@ const Carousel: TCarousel = React.forwardRef(
canScrollNext,
}))
const carousel = overlay
? (
<div
className={cn('relative', className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{overlay}
<div
ref={carouselRef}
className="overflow-hidden [border-radius:inherit]"
>
{children}
</div>
</div>
)
: (
<div
ref={carouselRef}
className={cn('relative overflow-hidden', className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
)
return (
<CarouselContext.Provider
value={{
@ -112,16 +142,7 @@ const Carousel: TCarousel = React.forwardRef(
canScrollNext,
}}
>
<div
ref={carouselRef}
// onKeyDownCapture={handleKeyDown}
className={cn('relative overflow-hidden', className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
{carousel}
</CarouselContext.Provider>
)
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

View File

@ -0,0 +1,121 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { marketplaceClient } from '@/service/client'
import { fetchPluginRecommendBanners } from './banners'
vi.mock('@/service/client', () => ({
marketplaceClient: {
banners: {
list: vi.fn(),
},
},
}))
const mockedListBanners = vi.mocked(marketplaceClient.banners.list)
describe('fetchPluginRecommendBanners', () => {
beforeEach(() => {
mockedListBanners.mockReset()
})
it('normalizes, sorts, and limits recommend banners from the public contract', async () => {
mockedListBanners.mockResolvedValue({
code: 0,
msg: 'success',
data: {
banners: [
{
id: 'event',
style_type: 'event',
title: 'Event',
sort: 0,
language: 'en',
content: {},
},
{
id: 'recommend-2',
style_type: 'recommend',
title: 'Second',
sort: 2,
language: 'en',
content: {
cards: [
{
item_type: 'plugin',
item_id: 'langgenius/fifth',
display_name: 'Fifth',
link: '/plugins/langgenius/fifth',
card_position: 4,
},
{
item_type: 'plugin',
item_id: 'langgenius/first',
display_name: 'First',
icon_url: '/api/v1/plugins/langgenius/first/icon',
link: '/plugins/langgenius/first',
card_position: 0,
},
{
item_type: 'plugin',
item_id: 'langgenius/third',
display_name: 'Third',
link: '/plugins/langgenius/third',
card_position: 2,
},
{
item_type: 'plugin',
item_id: 'langgenius/second',
display_name: 'Second',
link: '/plugins/langgenius/second',
card_position: 1,
},
{
item_type: 'plugin',
item_id: 'langgenius/fourth',
display_name: 'Fourth',
link: '/plugins/langgenius/fourth',
card_position: 3,
},
],
},
},
{
id: 'recommend-1',
style_type: 'recommend',
title: 'First',
sort: 1,
language: 'en',
content: {
cards: [
{
item_type: 'plugin',
item_id: 'langgenius/agent',
display_name: 'Agent',
link: '/plugins/langgenius/agent',
card_position: 0,
},
],
},
},
],
},
})
const banners = await fetchPluginRecommendBanners('en-US')
expect(mockedListBanners).toHaveBeenCalledWith({
query: {
page: 'plugins',
language: 'en-US',
},
})
expect(banners.map(banner => banner.id)).toEqual(['recommend-1', 'recommend-2'])
expect(banners[1]!.content.cards.map(card => card.display_name))
.toEqual(['First', 'Second', 'Third', 'Fourth'])
})
it('returns no banners for an empty response', async () => {
mockedListBanners.mockResolvedValue('')
await expect(fetchPluginRecommendBanners('en-US')).resolves.toEqual([])
})
})

View File

@ -0,0 +1,127 @@
import { marketplaceClient } from '@/service/client'
const MAX_TRENDING_PAGES = 3
const MAX_CARDS_PER_PAGE = 4
export type BannerRecommendCard = {
item_type: 'plugin' | 'template'
item_id: string
display_name: string
icon_url?: string
icon?: string
icon_background?: string
link: string
card_position: number
}
export type BannerRecommend = {
id: string
style_type: 'recommend'
title: string
sort: number
language: string
content: {
theme_type?: string
heading?: string
subheadings?: string[]
description?: string
cards: BannerRecommendCard[]
}
}
const isRecord = (value: unknown): value is Record<string, unknown> => {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
const parseRecommendCard = (value: unknown): BannerRecommendCard | null => {
if (!isRecord(value))
return null
const itemType = value.item_type
const itemId = value.item_id
const displayName = value.display_name
if (
(itemType !== 'plugin' && itemType !== 'template')
|| typeof itemId !== 'string'
|| !itemId
|| typeof displayName !== 'string'
|| !displayName
) {
return null
}
return {
item_type: itemType,
item_id: itemId,
display_name: displayName,
icon_url: typeof value.icon_url === 'string' ? value.icon_url : undefined,
icon: typeof value.icon === 'string' ? value.icon : undefined,
icon_background: typeof value.icon_background === 'string' ? value.icon_background : undefined,
link: typeof value.link === 'string' ? value.link : '',
card_position: typeof value.card_position === 'number' ? value.card_position : 0,
}
}
const parseRecommendBanner = (value: unknown): BannerRecommend | null => {
if (!isRecord(value) || value.style_type !== 'recommend' || !isRecord(value.content))
return null
const cards = Array.isArray(value.content.cards)
? value.content.cards
.map(parseRecommendCard)
.filter((card): card is BannerRecommendCard => Boolean(card))
.sort((a, b) => a.card_position - b.card_position)
.slice(0, MAX_CARDS_PER_PAGE)
: []
if (
typeof value.id !== 'string'
|| typeof value.title !== 'string'
|| typeof value.sort !== 'number'
|| typeof value.language !== 'string'
|| cards.length === 0
) {
return null
}
const subheadings = Array.isArray(value.content.subheadings)
? value.content.subheadings.filter((item): item is string => typeof item === 'string')
: undefined
return {
id: value.id,
style_type: 'recommend',
title: value.title,
sort: value.sort,
language: value.language,
content: {
theme_type: typeof value.content.theme_type === 'string' ? value.content.theme_type : undefined,
heading: typeof value.content.heading === 'string' ? value.content.heading : undefined,
subheadings,
description: typeof value.content.description === 'string' ? value.content.description : undefined,
cards,
},
}
}
export const normalizePluginRecommendBanners = (response: unknown): BannerRecommend[] => {
if (!isRecord(response) || !isRecord(response.data) || !Array.isArray(response.data.banners))
return []
return response.data.banners
.map(parseRecommendBanner)
.filter((banner): banner is BannerRecommend => Boolean(banner))
.sort((a, b) => a.sort - b.sort)
.slice(0, MAX_TRENDING_PAGES)
}
export const fetchPluginRecommendBanners = async (language: string): Promise<BannerRecommend[]> => {
const response = await marketplaceClient.banners.list({
query: {
page: 'plugins',
language,
},
})
return normalizePluginRecommendBanners(response)
}

View File

@ -0,0 +1,54 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from '#i18n'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import Link from '@/next/link'
type HomeHeaderProps = {
actions?: React.ReactNode
isMarketplacePlatform: boolean
}
const CreatorCenter = () => (
<Link href="https://creators.dify.ai/" target="_blank" rel="noopener noreferrer">
<Button
variant="ghost"
className="flex items-center gap-1 px-3 py-2 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
>
<span className="i-ri-user-star-line size-4" />
<span className="hidden system-sm-medium lg:inline">Creator Center</span>
</Button>
</Link>
)
const HomeHeader = ({
actions,
isMarketplacePlatform,
}: HomeHeaderProps) => {
const { t } = useTranslation('plugin')
return (
<header
className={cn(
'sticky top-0 z-50 flex w-full shrink-0 items-center justify-between bg-background-default px-4 backdrop-blur-sm md:px-9',
isMarketplacePlatform ? 'h-[46px]' : 'h-11',
)}
>
<Link href="/" className="flex h-full w-[142px] items-center">
<DifyLogo size="small" className="h-[18px] w-[39px] shrink-0" />
<span className="ml-1 whitespace-nowrap text-[13px] leading-[15px] font-semibold text-text-primary">
{t(($) => $['marketplace.difyMarketplace'])}
</span>
</Link>
<div className="flex h-full items-center gap-0.5">
<CreatorCenter />
{actions}
</div>
</header>
)
}
export default HomeHeader

View File

@ -0,0 +1,89 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from '#i18n'
import blueBrick from './assets/blue-brick.png'
import greenFlag from './assets/green-flag.png'
import magnifyingGlass from './assets/magnifying-glass.png'
import redBrick from './assets/red-brick.png'
import star from './assets/star.png'
import telescope from './assets/telescope.png'
type HomeHeroProps = {
isMarketplacePlatform: boolean
}
const HomeHero = ({ isMarketplacePlatform }: HomeHeroProps) => {
const { t } = useTranslation('plugin')
return (
<section
className={cn(
'relative flex shrink-0 justify-center bg-background-default px-4',
!isMarketplacePlatform && 'pt-6',
)}
>
<div className="relative flex h-[162px] w-full max-w-[726px] flex-col items-center pt-[41px]">
<div className="relative z-10 flex flex-col items-center gap-2 text-center">
<h1 className="text-[28px] leading-[34px] font-semibold tracking-[-0.56px] text-text-primary">
{t(($) => $['marketplace.home.heroTitle'])}
</h1>
<p className="text-[13px] leading-4 font-light tracking-[-0.065px] text-text-tertiary">
{t(($) => $['marketplace.home.heroSubtitle'])}
</p>
</div>
<img
src={blueBrick.src}
width={93}
height={91}
alt=""
aria-hidden
className="pointer-events-none absolute top-1/2 left-0 h-[91px] w-[93px] -translate-y-1/2 -rotate-[44deg] select-none"
/>
<img
src={magnifyingGlass.src}
width={26}
height={22}
alt=""
aria-hidden
className="pointer-events-none absolute top-[28px] left-[169px] hidden h-[22px] w-[26px] select-none opacity-[0.88] sm:block"
/>
<img
src={star.src}
width={16}
height={16}
alt=""
aria-hidden
className="pointer-events-none absolute top-3 left-[294px] hidden size-4 -scale-y-100 select-none opacity-[0.88] sm:block"
/>
<img
src={greenFlag.src}
width={24}
height={24}
alt=""
aria-hidden
className="pointer-events-none absolute top-0 left-[409px] hidden size-6 rotate-45 select-none opacity-[0.88] sm:block"
/>
<img
src={redBrick.src}
width={21}
height={20}
alt=""
aria-hidden
className="pointer-events-none absolute top-[23px] left-[541px] hidden h-5 w-[21px] -rotate-[60deg] select-none opacity-[0.88] sm:block"
/>
<img
src={telescope.src}
width={93}
height={93}
alt=""
aria-hidden
className="pointer-events-none absolute top-1/2 right-0 size-[93px] -translate-y-1/2 rotate-[15deg] select-none"
/>
</div>
</section>
)
}
export default HomeHero

View File

@ -0,0 +1,53 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
import { useEffect, useRef } from 'react'
import { useTranslation } from '#i18n'
import SearchBoxWrapper from '@/app/components/plugins/marketplace/search-box/search-box-wrapper'
type HomeSearchProps = {
isMarketplacePlatform: boolean
}
const HomeSearch = ({ isMarketplacePlatform }: HomeSearchProps) => {
const searchRef = useRef<HTMLDivElement>(null)
const { t } = useTranslation('plugin')
useEffect(() => {
const handleGlobalSearchShortcut = (event: KeyboardEvent) => {
if (event.key.toLowerCase() !== 'k' || (!event.metaKey && !event.ctrlKey))
return
event.preventDefault()
searchRef.current?.querySelector('input')?.focus()
}
document.addEventListener('keydown', handleGlobalSearchShortcut)
return () => document.removeEventListener('keydown', handleGlobalSearchShortcut)
}, [])
return (
<div
className={cn(
'sticky z-[60] -mt-9 flex h-9 shrink-0 justify-center px-4',
isMarketplacePlatform ? 'top-[5px]' : 'top-1',
)}
>
<div ref={searchRef} className="relative w-full max-w-[480px]">
<SearchBoxWrapper
wrapperClassName="w-full max-w-none"
inputClassName="h-9 w-full rounded-[10px] border-divider-subtle bg-components-input-bg-normal shadow-xs"
inputElementClassName="pr-12"
placeholder={t(($) => $['marketplace.home.searchPlaceholder'])}
showTags={false}
usedInMarketplace={false}
/>
<kbd className="pointer-events-none absolute top-1/2 right-2 flex h-5 -translate-y-1/2 items-center rounded-md border border-divider-subtle bg-components-kbd-bg-gray px-1.5 font-sans text-[10px] leading-3 font-medium text-text-tertiary shadow-xs">
K
</kbd>
</div>
</div>
)
}
export default HomeSearch

View File

@ -0,0 +1,33 @@
@property --trending-progress-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.progress {
--trending-progress-angle: 0deg;
position: absolute;
inset: 0;
border-radius: 7px;
background: conic-gradient(
from 0deg,
var(--color-text-primary) var(--trending-progress-angle),
transparent var(--trending-progress-angle)
);
animation-name: progress;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
@keyframes progress {
to {
--trending-progress-angle: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.progress {
animation: none;
}
}

View File

@ -0,0 +1,377 @@
'use client'
import type { FocusEvent } from 'react'
import type { BannerRecommend, BannerRecommendCard } from './banners'
import { cn } from '@langgenius/dify-ui/cn'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from '#i18n'
import { Carousel, useCarousel } from '@/app/components/base/carousel'
import { MARKETPLACE_API_PREFIX } from '@/config'
import Link from '@/next/link'
import background from './assets/background.jpg'
import styles from './home-trending-indicator.module.css'
const AUTOPLAY_DELAY = 5000
type TrendingIndicatorProps = {
index: number
label: string
isCurrent: boolean
isNextSlide: boolean
isPaused: boolean
onClick: () => void
}
const TrendingIndicator = ({
index,
label,
isCurrent,
isNextSlide,
isPaused,
onClick,
}: TrendingIndicatorProps) => {
return (
<button
type="button"
aria-label={label}
aria-current={isCurrent ? 'true' : undefined}
onClick={onClick}
className="group relative flex size-6 shrink-0 items-center justify-center rounded-lg p-0 hover:bg-transparent"
>
<span
className={cn(
'relative flex h-5 w-[22px] items-center justify-center overflow-hidden rounded-[7px] p-px ring-1 ring-divider-subtle ring-inset',
isCurrent && 'bg-text-primary ring-text-primary',
)}
>
{isNextSlide && !isCurrent && !isPaused
? (
<span
data-progress-ring
className={styles.progress}
aria-hidden="true"
style={{ animationDuration: `${AUTOPLAY_DELAY}ms` }}
/>
)
: null}
<span className="relative z-10 flex h-[18px] w-5 items-center justify-center rounded-md bg-components-panel-on-panel-item-bg p-0.5 text-center text-[10px] leading-3 font-semibold text-text-tertiary transition-colors group-hover:text-text-secondary group-aria-[current=true]:bg-text-primary group-aria-[current=true]:text-components-panel-on-panel-item-bg">
{String(index + 1).padStart(2, '0')}
</span>
</span>
</button>
)
}
type TrendingCopyProps = {
banners: BannerRecommend[]
isMarketplacePlatform: boolean
}
const TrendingCopy = ({
banners,
isMarketplacePlatform,
}: TrendingCopyProps) => {
const { t } = useTranslation('plugin')
const { api, selectedIndex } = useCarousel()
const [isPlaying, setIsPlaying] = useState(false)
const shouldResumeAfterFocusRef = useRef(false)
const nextIndex = (selectedIndex + 1) % banners.length
const pauseRotationForFocus = () => {
const autoplay = api?.plugins().autoplay
if (!autoplay?.isPlaying())
return
shouldResumeAfterFocusRef.current = true
autoplay.stop()
}
const resumeRotationAfterFocus = (event: FocusEvent<HTMLDivElement>) => {
if (event.currentTarget.contains(event.relatedTarget))
return
if (!shouldResumeAfterFocusRef.current)
return
shouldResumeAfterFocusRef.current = false
api?.plugins().autoplay?.play()
}
useEffect(() => {
if (!api)
return
const handleAutoplayPlay = () => setIsPlaying(true)
const handleAutoplayStop = () => setIsPlaying(false)
// oxlint-disable-next-line eslint-react/set-state-in-effect -- Embla owns this external playback state.
setIsPlaying(api.plugins().autoplay?.isPlaying() ?? false)
api.on('autoplay:play', handleAutoplayPlay)
api.on('autoplay:stop', handleAutoplayStop)
return () => {
api.off('autoplay:play', handleAutoplayPlay)
api.off('autoplay:stop', handleAutoplayStop)
}
}, [api])
return (
<div
className={cn(
'relative flex h-[200px] w-full flex-col gap-2.5 overflow-hidden bg-background-body p-4',
isMarketplacePlatform
? 'min-[1232px]:absolute min-[1232px]:top-0 min-[1232px]:right-full min-[1232px]:w-[443px]'
: 'min-[1260px]:absolute min-[1260px]:top-0 min-[1260px]:right-full min-[1260px]:w-[431px]',
)}
>
<div className="flex min-h-0 flex-1 flex-col items-start gap-2 overflow-hidden">
<p className="shrink-0 text-[10px] leading-3 font-semibold tracking-[-0.2px] text-text-accent">
{t(($) => $['marketplace.home.trendingEyebrow'])}
</p>
<h2
id="home-trending-title"
className="shrink-0 text-xl leading-6 font-semibold tracking-[-0.4px] text-text-primary"
>
{t(($) => $['marketplace.home.trendingTitle'])}
</h2>
<p className="text-[13px] leading-5 font-normal tracking-[-0.065px] text-text-tertiary">
{t(($) => $['marketplace.home.trendingDescription'])}
</p>
</div>
<div
role="group"
aria-label={t(($) => $['marketplace.home.trendingPaginationLabel'])}
className="flex shrink-0 items-center py-1 pr-10"
onFocusCapture={pauseRotationForFocus}
onBlurCapture={resumeRotationAfterFocus}
>
<div className="flex items-center gap-0.5">
{banners.map((banner, index) => (
<TrendingIndicator
key={banner.id}
index={index}
label={`${String(index + 1).padStart(2, '0')} ${banner.title}`}
isCurrent={index === selectedIndex}
isNextSlide={index === nextIndex}
isPaused={!isPlaying}
onClick={() => api?.scrollTo(index)}
/>
))}
</div>
</div>
</div>
)
}
const getMarketplaceAssetURL = (path?: string) => {
if (!path)
return ''
if (/^https?:\/\//.test(path))
return path
try {
const apiURL = new URL(MARKETPLACE_API_PREFIX)
if (path.startsWith('/api/'))
return `${apiURL.origin}${path}`
return `${MARKETPLACE_API_PREFIX.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
}
catch {
return path
}
}
const getLocalCardHref = (card: BannerRecommendCard) => {
if (card.item_type === 'plugin') {
const [organization, pluginName] = card.item_id.split('/')
if (organization && pluginName)
return `/plugin/${encodeURIComponent(organization)}/${encodeURIComponent(pluginName)}`
}
if (card.item_type === 'template')
return `/templates?tid=${encodeURIComponent(card.item_id)}`
return '/'
}
const getCardHref = (
card: BannerRecommendCard,
isMarketplacePlatform: boolean,
) => {
if (!isMarketplacePlatform && card.link)
return card.link
return getLocalCardHref(card)
}
const getCardCreator = (card: BannerRecommendCard) => {
if (card.item_type !== 'plugin')
return ''
return card.item_id.split('/')[0] || ''
}
type TrendingCardProps = {
card: BannerRecommendCard
isMarketplacePlatform: boolean
}
const TrendingCard = ({
card,
isMarketplacePlatform,
}: TrendingCardProps) => {
const { t } = useTranslation('plugin')
const iconURL = getMarketplaceAssetURL(card.icon_url)
const creator = getCardCreator(card)
const href = getCardHref(card, isMarketplacePlatform)
const opensInNewTab = !isMarketplacePlatform && /^https?:\/\//.test(href)
return (
<Link
href={href}
target={opensInNewTab ? '_blank' : undefined}
rel={opensInNewTab ? 'noopener noreferrer' : undefined}
aria-label={card.display_name}
className="relative flex h-[116px] w-[161px] shrink-0 flex-col items-start overflow-hidden rounded-lg bg-components-panel-on-panel-item-bg-transparent p-3.5 shadow-md backdrop-blur-md"
>
<div
className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-[10px] border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge bg-cover bg-center bg-no-repeat"
style={{
backgroundColor: !iconURL ? card.icon_background : undefined,
backgroundImage: iconURL ? `url(${iconURL})` : undefined,
}}
>
{!iconURL && card.icon
? <span className="text-xl leading-none">{card.icon}</span>
: null}
{!iconURL && !card.icon
? <span aria-hidden="true" className="i-ri-image-line size-5 text-text-quaternary" />
: null}
</div>
<h3 className="mt-3 w-full truncate text-sm leading-[normal] font-medium text-text-primary">
{card.display_name}
</h3>
{creator
? (
<p className="mt-[3px] w-full truncate text-xs leading-[normal] font-normal text-text-tertiary">
{t(($) => $['marketplace.home.trendingByCreator'], { creator })}
</p>
)
: null}
<span
aria-hidden="true"
className="i-ri-arrow-right-up-line absolute top-1.5 right-2 size-4 text-text-quaternary"
/>
</Link>
)
}
type TrendingSlideProps = {
banner: BannerRecommend
isMarketplacePlatform: boolean
}
const TrendingSlide = ({
banner,
isMarketplacePlatform,
}: TrendingSlideProps) => {
return (
<div className="relative h-[200px] w-full overflow-hidden rounded-xl bg-text-accent">
<img
src={background.src}
width={3840}
height={2160}
alt=""
aria-hidden
className="absolute top-[-173px] left-[-990px] h-[1201px] w-[2135px] max-w-none opacity-80"
/>
<div aria-hidden className="absolute inset-0 bg-text-accent mix-blend-color" />
<div className="relative z-10 flex h-full items-center justify-between px-9 py-[42px]">
{banner.content.cards.map(card => (
<TrendingCard
key={`${card.item_type}:${card.item_id}`}
card={card}
isMarketplacePlatform={isMarketplacePlatform}
/>
))}
</div>
</div>
)
}
type HomeTrendingProps = {
banners: BannerRecommend[]
isMarketplacePlatform: boolean
}
const HomeTrending = ({
banners,
isMarketplacePlatform,
}: HomeTrendingProps) => {
const { t } = useTranslation('plugin')
const [carouselPlugins] = useState(() => [
Carousel.Plugin.Autoplay({
delay: AUTOPLAY_DELAY,
stopOnFocusIn: true,
stopOnInteraction: false,
stopOnMouseEnter: true,
breakpoints: {
'(prefers-reduced-motion: reduce)': { active: false },
},
}),
])
if (banners.length === 0)
return null
return (
<section
aria-labelledby="home-trending-title"
className={cn(
'shrink-0 bg-background-default pb-6',
isMarketplacePlatform
? 'px-4 min-[1232px]:px-0'
: 'px-4 md:px-9',
)}
>
<div
className={cn(
'mx-auto w-full overflow-hidden rounded-xl bg-background-body',
isMarketplacePlatform ? 'max-w-[1200px]' : 'max-w-[1188px]',
)}
>
<Carousel
opts={{ loop: true }}
plugins={carouselPlugins}
overlay={(
<TrendingCopy
banners={banners}
isMarketplacePlatform={isMarketplacePlatform}
/>
)}
aria-label={t(($) => $['marketplace.home.trendingTitle'])}
className={cn(
'ml-auto w-full rounded-xl',
isMarketplacePlatform
? 'min-[1232px]:w-[757px]'
: 'min-[1260px]:w-[757px]',
)}
>
<Carousel.Content aria-live="polite" className="rounded-xl">
{banners.map(banner => (
<Carousel.Item key={banner.id}>
<TrendingSlide
banner={banner}
isMarketplacePlatform={isMarketplacePlatform}
/>
</Carousel.Item>
))}
</Carousel.Content>
</Carousel>
</div>
</section>
)
}
export default HomeTrending

View File

@ -0,0 +1,52 @@
import type { BannerRecommend } from './banners'
import { cn } from '@langgenius/dify-ui/cn'
import ListWrapper from '../list/list-wrapper'
import HomeHeader from './home-header'
import HomeHero from './home-hero'
import HomeSearch from './home-search'
import HomeTrending from './home-trending'
type MarketplaceHomeProps = {
actions?: React.ReactNode
banners: BannerRecommend[]
isMarketplacePlatform: boolean
linkToMarketplaceDetail: boolean
showInstallButton: boolean
}
const MarketplaceHome = ({
actions,
banners,
isMarketplacePlatform,
linkToMarketplaceDetail,
showInstallButton,
}: MarketplaceHomeProps) => {
return (
<div className="flex min-h-full w-full flex-col bg-background-default">
<HomeHeader
actions={actions}
isMarketplacePlatform={isMarketplacePlatform}
/>
<div className="relative flex w-full flex-col">
<HomeHero isMarketplacePlatform={isMarketplacePlatform} />
<HomeSearch isMarketplacePlatform={isMarketplacePlatform} />
<div
aria-hidden="true"
className={cn('shrink-0', isMarketplacePlatform ? 'h-6' : 'h-12')}
/>
<HomeTrending
banners={banners}
isMarketplacePlatform={isMarketplacePlatform}
/>
<div className="contents [&>div]:bg-background-default!">
<ListWrapper
showInstallButton={showInstallButton}
linkToMarketplaceDetail={linkToMarketplaceDetail}
/>
</div>
</div>
</div>
)
}
export default MarketplaceHome

View File

@ -1,17 +1,26 @@
import type { SearchParams } from 'nuqs'
import type { BannerRecommend } from './home/banners'
import { PluginInstallPermissionProviderGuard } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider'
import { TanStackQueryProvider } from '@/app/query-provider'
import { getLocaleOnServer } from '@/i18n-config/server'
import Description from './description'
import MarketplaceHome from './home'
import { fetchPluginRecommendBanners } from './home/banners'
import { HydrateQueryClient } from './hydration-server'
import ListWrapper from './list/list-wrapper'
import StickySearchAndSwitchWrapper from './sticky-search-and-switch-wrapper'
type MarketplaceVariant = 'default' | 'home'
type MarketplaceProps = {
showInstallButton?: boolean
linkToMarketplaceDetail?: boolean
pluginTypeSwitchClassName?: string
isMarketplacePlatform?: boolean
marketplaceNav?: React.ReactNode
variant?: MarketplaceVariant
language?: string
homeHeaderActions?: React.ReactNode
/**
* Pass the search params from the request to prefetch data on the server.
*/
@ -24,23 +33,53 @@ const Marketplace = async ({
pluginTypeSwitchClassName,
isMarketplacePlatform = false,
marketplaceNav,
variant = 'default',
language,
homeHeaderActions,
searchParams,
}: MarketplaceProps) => {
let trendingBanners: BannerRecommend[] = []
if (variant === 'home') {
const locale = language ?? await getLocaleOnServer()
try {
trendingBanners = await fetchPluginRecommendBanners(locale)
}
catch {
// Keep the homepage available if Marketplace banner delivery is unavailable.
}
}
return (
<TanStackQueryProvider>
<HydrateQueryClient searchParams={searchParams}>
<PluginInstallPermissionProviderGuard canInstallPlugin={showInstallButton}>
<Description
isMarketplacePlatform={isMarketplacePlatform}
marketplaceNav={marketplaceNav}
/>
{!isMarketplacePlatform && (
<StickySearchAndSwitchWrapper pluginTypeSwitchClassName={pluginTypeSwitchClassName} />
)}
<ListWrapper
showInstallButton={showInstallButton}
linkToMarketplaceDetail={linkToMarketplaceDetail}
/>
{variant === 'home'
? (
<MarketplaceHome
actions={homeHeaderActions}
banners={trendingBanners}
isMarketplacePlatform={isMarketplacePlatform}
linkToMarketplaceDetail={linkToMarketplaceDetail}
showInstallButton={showInstallButton}
/>
)
: (
<>
<Description
isMarketplacePlatform={isMarketplacePlatform}
marketplaceNav={marketplaceNav}
/>
{!isMarketplacePlatform && (
<StickySearchAndSwitchWrapper pluginTypeSwitchClassName={pluginTypeSwitchClassName} />
)}
<ListWrapper
showInstallButton={showInstallButton}
linkToMarketplaceDetail={linkToMarketplaceDetail}
/>
</>
)}
</PluginInstallPermissionProviderGuard>
</HydrateQueryClient>
</TanStackQueryProvider>

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "سوق Dify",
"marketplace.discover": "اكتشف",
"marketplace.empower": "تمكين تطوير الذكاء الاصطناعي الخاص بك",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "المزيد من السوق",
"marketplace.noPluginFound": "لم يتم العثور على إضافة",
"marketplace.partnerTip": "تم التحقق بواسطة شريك Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify Marktplatz",
"marketplace.discover": "Entdecken",
"marketplace.empower": "Unterstützen Sie Ihre KI-Entwicklung",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Mehr aus dem Marketplace",
"marketplace.noPluginFound": "Kein Plugin gefunden",
"marketplace.partnerTip": "Von einem Dify-Partner verifiziert",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Discover",
"marketplace.empower": "Empower your AI development",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "More from Marketplace",
"marketplace.noPluginFound": "No integration found",
"marketplace.partnerTip": "Verified by a Dify partner",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Mercado de Dify",
"marketplace.discover": "Descubrir",
"marketplace.empower": "Potencie su desarrollo de IA",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Más de Marketplace",
"marketplace.noPluginFound": "No se ha encontrado ninguna integración",
"marketplace.partnerTip": "Verificado por un socio de Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "بازار دیفی",
"marketplace.discover": "کشف",
"marketplace.empower": "توسعه هوش مصنوعی خود را توانمند کنید",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "اطلاعات بیشتر از Marketplace",
"marketplace.noPluginFound": "هیچ افزونه‌ای یافت نشد",
"marketplace.partnerTip": "تأیید شده توسط یک شریک دیفی",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Marché Dify",
"marketplace.discover": "Découvrir",
"marketplace.empower": "Renforcez le développement de votre IA",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Plus de Marketplace",
"marketplace.noPluginFound": "Aucune intégration trouvée",
"marketplace.partnerTip": "Vérifié par un partenaire Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "डिफाई मार्केटप्लेस",
"marketplace.discover": "खोजें",
"marketplace.empower": "अपने एआई विकास को सशक्त बनाएं",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "मार्केटप्लेस से अधिक",
"marketplace.noPluginFound": "कोई इंटीग्रेशन नहीं मिला",
"marketplace.partnerTip": "Dify भागीदार द्वारा सत्यापित",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Menemukan",
"marketplace.empower": "Berdayakan pengembangan AI Anda",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Selengkapnya dari Marketplace",
"marketplace.noPluginFound": "Tidak ada integrasi yang ditemukan",
"marketplace.partnerTip": "Diverifikasi oleh partner Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Mercato Dify",
"marketplace.discover": "Scoprire",
"marketplace.empower": "Potenzia lo sviluppo dell'intelligenza artificiale",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Altro da Marketplace",
"marketplace.noPluginFound": "Nessuna integrazione trovata",
"marketplace.partnerTip": "Verificato da un partner Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify マーケットプレイス",
"marketplace.discover": "探索",
"marketplace.empower": "AI 開発をサポートする",
"marketplace.home.heroSubtitle": "Dify Marketplace で、より安全で信頼性の高いプラグインを見つけましょう。",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "プラグインまたはテンプレートを検索",
"marketplace.home.trendingByCreator": "{{creator}} 作成",
"marketplace.home.trendingDescription": "実際の利用状況に基づく人気プラグインを2週間ごとに更新。ワークスペースでの実行数によるランキングで、有料掲載や編集部による選定はありません。",
"marketplace.home.trendingEyebrow": "トレンド",
"marketplace.home.trendingPaginationLabel": "トレンドページ",
"marketplace.home.trendingTitle": "みんながインストールしているプラグイン",
"marketplace.moreFrom": "マーケットプレイスからのさらなる情報",
"marketplace.noPluginFound": "インテグレーションが見つかりません",
"marketplace.partnerTip": "このプラグインは Dify のパートナーによって認証されています",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify 마켓플레이스",
"marketplace.discover": "발견하다",
"marketplace.empower": "AI 개발 역량 강화",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Marketplace 에서 더 보기",
"marketplace.noPluginFound": "플러그인을 찾을 수 없습니다.",
"marketplace.partnerTip": "Dify 파트너에 의해 확인됨",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Discover",
"marketplace.empower": "Empower your AI development",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "More from Marketplace",
"marketplace.noPluginFound": "Geen plugin gevonden",
"marketplace.partnerTip": "Verified by a Dify partner",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Rynek Dify",
"marketplace.discover": "Odkryć",
"marketplace.empower": "Zwiększ możliwości rozwoju sztucznej inteligencji",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Więcej z Marketplace",
"marketplace.noPluginFound": "Nie znaleziono integracji",
"marketplace.partnerTip": "Zweryfikowane przez partnera Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Mercado Dify",
"marketplace.discover": "Descobrir",
"marketplace.empower": "Capacite seu desenvolvimento de IA",
"marketplace.home.heroSubtitle": "Crie com plugins mais seguros e confiáveis do Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Buscar plugins ou modelos",
"marketplace.home.trendingByCreator": "por {{creator}}",
"marketplace.home.trendingDescription": "Destaques por uso real, atualizados a cada duas semanas. Classificados pelas execuções reais nos espaços de trabalho — sem promoção paga ou seleção editorial.",
"marketplace.home.trendingEyebrow": "Em alta agora",
"marketplace.home.trendingPaginationLabel": "Páginas em alta",
"marketplace.home.trendingTitle": "Os plugins que todos estão instalando",
"marketplace.moreFrom": "Mais do Marketplace",
"marketplace.noPluginFound": "Nenhuma integração encontrada",
"marketplace.partnerTip": "Verificado por um parceiro da Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Piața Dify",
"marketplace.discover": "Descoperi",
"marketplace.empower": "Îmbunătățește-ți dezvoltarea AI",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Mai multe din Marketplace",
"marketplace.noPluginFound": "Nu s-a găsit niciun plugin",
"marketplace.partnerTip": "Verificat de un partener Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Торговая площадка Dify",
"marketplace.discover": "Обнаруживать",
"marketplace.empower": "Расширьте возможности разработки ИИ",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Больше из Marketplace",
"marketplace.noPluginFound": "Плагин не найден",
"marketplace.partnerTip": "Подтверждено партнером Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Odkrijte",
"marketplace.empower": "Okrepite svoj razvoj AI",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Več iz tržnice",
"marketplace.noPluginFound": "Nobenega vtičnika ni bilo najti.",
"marketplace.partnerTip": "Potrjeno s strani partnerja Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "ตลาด Dify",
"marketplace.discover": "ค้นพบ",
"marketplace.empower": "เพิ่มศักยภาพในการพัฒนา AI ของคุณ",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "แอปเพิ่มเติมจาก Marketplace",
"marketplace.noPluginFound": "ไม่พบปลั๊กอิน",
"marketplace.partnerTip": "ได้รับการตรวจสอบโดยพันธมิตรของ Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify Pazar Yeri",
"marketplace.discover": "Keşfet",
"marketplace.empower": "Yapay zeka geliştirmenizi güçlendirin",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Pazar Yeri'nden daha fazlası",
"marketplace.noPluginFound": "Eklenti bulunamadı",
"marketplace.partnerTip": "Dify partner'ı tarafından doğrulandı",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Виявити",
"marketplace.empower": "Розширюйте можливості розробки штучного інтелекту",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Більше від Marketplace",
"marketplace.noPluginFound": "Плагін не знайдено",
"marketplace.partnerTip": "Перевірено партнером Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Thị trường Dify",
"marketplace.discover": "Khám phá",
"marketplace.empower": "Hỗ trợ phát triển AI của bạn",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "Search plugins or templates",
"marketplace.home.trendingByCreator": "by {{creator}}",
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.moreFrom": "Các ứng dụng khác từ Marketplace",
"marketplace.noPluginFound": "Không tìm thấy plugin nào",
"marketplace.partnerTip": "Được xác nhận bởi một đối tác của Dify",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "探索",
"marketplace.empower": "助力您的 AI 开发",
"marketplace.home.heroSubtitle": "在 Dify Marketplace 探索更安全、更可靠的插件。",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "搜索插件或模板",
"marketplace.home.trendingByCreator": "由 {{creator}} 发布",
"marketplace.home.trendingDescription": "基于真实使用情况选出的热门插件,每两周更新一次。榜单按各工作区的实际运行次数排序,不含付费推广或编辑推荐。",
"marketplace.home.trendingEyebrow": "当前热门",
"marketplace.home.trendingPaginationLabel": "热门推荐页码",
"marketplace.home.trendingTitle": "大家都在安装的插件",
"marketplace.moreFrom": "来自 Marketplace 的更多内容",
"marketplace.noPluginFound": "未找到集成",
"marketplace.partnerTip": "此插件由 Dify 合作伙伴认证",

View File

@ -226,6 +226,14 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "發現",
"marketplace.empower": "為您的 AI 開發提供支援",
"marketplace.home.heroSubtitle": "在 Dify Marketplace 探索更安全、更可靠的外掛程式。",
"marketplace.home.heroTitle": "Discover. Extend. Build",
"marketplace.home.searchPlaceholder": "搜尋外掛程式或範本",
"marketplace.home.trendingByCreator": "由 {{creator}} 發布",
"marketplace.home.trendingDescription": "根據真實使用情況選出的熱門外掛程式,每兩週更新一次。榜單按各工作區的實際執行次數排序,不含付費推廣或編輯推薦。",
"marketplace.home.trendingEyebrow": "目前熱門",
"marketplace.home.trendingPaginationLabel": "熱門推薦頁碼",
"marketplace.home.trendingTitle": "大家都在安裝的外掛程式",
"marketplace.moreFrom": "來自 Marketplace 的更多內容",
"marketplace.noPluginFound": "未找到集成",
"marketplace.partnerTip": "由 Dify 合作夥伴驗證",