From 0fc3791bb62f2e8dcb074ba3d22b33ee55b17d10 Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Sun, 16 Aug 2026 15:04:39 +0800 Subject: [PATCH] refactor(web): split marketplace home trending and share catalog constants Break the trending banner into navigation and slide modules with shared home constants, hoist the marketplace container id and partner-collection constants for reuse across the catalog, and drop the i18n keys the redesigned home no longer renders. Co-authored-by: Cursor --- web/app/(commonLayout)/marketplace/page.tsx | 3 +- .../templates/[[...category]]/page.tsx | 3 +- .../plugins/marketplace/constants.ts | 6 + .../plugins/marketplace/description/index.tsx | 3 +- .../home-catalog-navigation.spec.tsx | 4 - .../home/__tests__/home-header.spec.tsx | 1 - .../home/__tests__/home-trending.spec.tsx | 59 ++ .../home/home-catalog-navigation.tsx | 10 +- .../marketplace/home/home-constants.ts | 6 + .../plugins/marketplace/home/home-header.tsx | 7 +- .../marketplace/home/home-sticky.module.css | 7 +- .../home/home-trending-navigation.tsx | 260 ++++++++ .../marketplace/home/home-trending-slides.tsx | 331 ++++++++++ .../marketplace/home/home-trending.tsx | 570 +----------------- .../components/plugins/marketplace/hooks.ts | 2 +- .../plugins/marketplace/list/carousel.tsx | 3 +- .../marketplace/list/collection-constants.ts | 6 + .../marketplace/list/list-with-collection.tsx | 9 +- web/i18n/ar-TN/plugin.json | 2 - web/i18n/de-DE/plugin.json | 2 - web/i18n/en-US/plugin.json | 2 - web/i18n/es-ES/plugin.json | 2 - web/i18n/fa-IR/plugin.json | 2 - web/i18n/fr-FR/plugin.json | 2 - web/i18n/hi-IN/plugin.json | 2 - web/i18n/id-ID/plugin.json | 2 - web/i18n/it-IT/plugin.json | 2 - web/i18n/ja-JP/plugin.json | 2 - web/i18n/ko-KR/plugin.json | 2 - web/i18n/lo-LA/plugin.json | 9 + web/i18n/nl-NL/plugin.json | 2 - web/i18n/pl-PL/plugin.json | 2 - web/i18n/pt-BR/plugin.json | 2 - web/i18n/ro-RO/plugin.json | 2 - web/i18n/ru-RU/plugin.json | 2 - web/i18n/sl-SI/plugin.json | 2 - web/i18n/th-TH/plugin.json | 2 - web/i18n/tr-TR/plugin.json | 2 - web/i18n/uk-UA/plugin.json | 2 - web/i18n/vi-VN/plugin.json | 2 - web/i18n/zh-Hans/plugin.json | 2 - web/i18n/zh-Hant/plugin.json | 2 - 42 files changed, 706 insertions(+), 639 deletions(-) create mode 100644 web/app/components/plugins/marketplace/home/home-constants.ts create mode 100644 web/app/components/plugins/marketplace/home/home-trending-navigation.tsx create mode 100644 web/app/components/plugins/marketplace/home/home-trending-slides.tsx diff --git a/web/app/(commonLayout)/marketplace/page.tsx b/web/app/(commonLayout)/marketplace/page.tsx index 34ddeda0516..e10590f5d98 100644 --- a/web/app/(commonLayout)/marketplace/page.tsx +++ b/web/app/(commonLayout)/marketplace/page.tsx @@ -1,5 +1,6 @@ import type { SearchParams } from 'nuqs' import AccountSection from '@/app/components/main-nav/components/account-section' +import { MARKETPLACE_CONTAINER_ID } from '@/app/components/plugins/marketplace/constants' import { EmbeddedMarketplace } from '@/app/components/plugins/marketplace/embedded' import { fetchPluginBanners } from '@/app/components/plugins/marketplace/home/banners' import { HydrateQueryClient } from '@/app/components/plugins/marketplace/hydration-server' @@ -16,7 +17,7 @@ const MarketplacePage = async ({ searchParams }: MarketplacePageProps) => { return (
diff --git a/web/app/(commonLayout)/templates/[[...category]]/page.tsx b/web/app/(commonLayout)/templates/[[...category]]/page.tsx index 08209f2a6a3..db134a6af3a 100644 --- a/web/app/(commonLayout)/templates/[[...category]]/page.tsx +++ b/web/app/(commonLayout)/templates/[[...category]]/page.tsx @@ -1,3 +1,4 @@ +import { MARKETPLACE_CONTAINER_ID } from '@/app/components/plugins/marketplace/constants' import { EmbeddedTemplatesMarketplace } from '@/app/components/plugins/marketplace/templates' import { isTemplateCategory } from '@/app/components/plugins/marketplace/templates/categories' import { getLocaleOnServer } from '@/i18n-config/server' @@ -51,7 +52,7 @@ export default async function TemplatesPage({ params, searchParams }: TemplatesP return (
{ const { t } = useTranslation('plugin') const { t: tCommon } = useTranslation('common') diff --git a/web/app/components/plugins/marketplace/home/__tests__/home-catalog-navigation.spec.tsx b/web/app/components/plugins/marketplace/home/__tests__/home-catalog-navigation.spec.tsx index 1521d39a3cc..4746c0ae915 100644 --- a/web/app/components/plugins/marketplace/home/__tests__/home-catalog-navigation.spec.tsx +++ b/web/app/components/plugins/marketplace/home/__tests__/home-catalog-navigation.spec.tsx @@ -50,11 +50,9 @@ describe('HomeCatalogNavigation', () => { expect(activeTab).toHaveClass('bg-state-base-active') expect(activeTab).not.toHaveClass('text-text-accent') expect(activeTab.querySelector('[aria-hidden="true"]')).not.toBeInTheDocument() - expect(screen.queryByText('plugin.marketplace.home.new')).not.toBeInTheDocument() expect( screen.getByRole('link', { name: /plugin\.marketplace\.home\.templates/ }), ).toHaveAttribute('href', '/templates') - expect(screen.queryByText('plugin.marketplace.home.new')).not.toBeInTheDocument() expect(screen.getByTestId('plugin-type-switch')).toHaveAttribute('data-variant', 'home') }) @@ -71,7 +69,6 @@ describe('HomeCatalogNavigation', () => { expect(templatesTab).toHaveAttribute('href', '/templates') expect(templatesTab).toHaveClass('cursor-pointer') expect(templatesTab).not.toHaveClass('bg-state-base-active') - expect(screen.queryByText('plugin.marketplace.home.new')).not.toBeInTheDocument() }) it('marks Templates as active when rendering the Templates catalog', () => { @@ -87,7 +84,6 @@ describe('HomeCatalogNavigation', () => { expect(templatesTab).toHaveClass('bg-state-base-active') expect(templatesTab).not.toHaveClass('text-text-accent') expect(templatesTab.querySelector('[aria-hidden="true"]')).not.toBeInTheDocument() - expect(screen.queryByText('plugin.marketplace.home.new')).not.toBeInTheDocument() }) it('uses request-localized labels and preserves the selected language', () => { diff --git a/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx b/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx index f47b027d39c..3f8ca437f6e 100644 --- a/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx +++ b/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx @@ -133,6 +133,5 @@ describe('HomeHeader', () => { expect(templatesTab).toHaveClass('bg-state-base-active') expect(templatesTab).not.toHaveClass('text-text-accent') expect(templatesTab.querySelector('[aria-hidden="true"]')).not.toBeInTheDocument() - expect(screen.queryByText('marketplace.home.new')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/marketplace/home/__tests__/home-trending.spec.tsx b/web/app/components/plugins/marketplace/home/__tests__/home-trending.spec.tsx index 52c11de565c..6cae858d79d 100644 --- a/web/app/components/plugins/marketplace/home/__tests__/home-trending.spec.tsx +++ b/web/app/components/plugins/marketplace/home/__tests__/home-trending.spec.tsx @@ -21,6 +21,11 @@ vi.mock('@/app/components/plugins/base/badges/verified', () => ({ default: () => , })) +vi.mock('@/config', async (importOriginal) => ({ + ...(await importOriginal()), + MARKETPLACE_URL_PREFIX: 'https://marketplace.example.com', +})) + const banners: PluginBanner[] = [ { id: 'recommend', @@ -410,6 +415,60 @@ describe('HomeTrending', () => { }) }) + it('sends embedded cards without a delivery link to the marketplace site', () => { + const bannerWithMixedLinks: PluginBanner = { + id: 'recommend-mixed', + style_type: 'recommend', + title: 'Trending', + sort: 0, + language: 'en', + content: { + theme_type: 'hottest', + cards: [ + { + item_type: 'plugin', + item_id: 'langgenius/dropbox', + display_name: 'Dropbox', + link: 'https://external.example.com/dropbox', + card_position: 0, + }, + { + // The console has no local /plugin route, so a card without a + // delivery-provided link must open the marketplace detail page. + item_type: 'plugin', + item_id: 'langgenius/notion', + display_name: 'Notion', + link: '', + card_position: 1, + }, + { + item_type: 'template', + item_id: 'tpl-1', + display_name: 'Support Bot', + link: '', + card_position: 2, + }, + ], + }, + } + + render() + + expect(screen.getByRole('link', { name: 'Dropbox' })).toHaveAttribute( + 'href', + 'https://external.example.com/dropbox', + ) + const marketplaceFallbackLink = screen.getByRole('link', { name: 'Notion' }) + expect(marketplaceFallbackLink.getAttribute('href')).toMatch( + /^https:\/\/marketplace\.example\.com\/plugins\/langgenius\/notion/, + ) + expect(marketplaceFallbackLink).toHaveAttribute('target', '_blank') + expect(screen.getByRole('link', { name: 'Support Bot' })).toHaveAttribute( + 'href', + '/templates?tid=tpl-1', + ) + }) + it('renders no carousel when the API returns no banners', () => { render() diff --git a/web/app/components/plugins/marketplace/home/home-catalog-navigation.tsx b/web/app/components/plugins/marketplace/home/home-catalog-navigation.tsx index 9194ee32b12..fc664eafb5f 100644 --- a/web/app/components/plugins/marketplace/home/home-catalog-navigation.tsx +++ b/web/app/components/plugins/marketplace/home/home-catalog-navigation.tsx @@ -5,7 +5,9 @@ import { cn } from '@langgenius/dify-ui/cn' import { useAtomValue, useSetAtom } from 'jotai' import { useEffect, useRef } from 'react' import { useTranslation } from '#i18n' +import { MARKETPLACE_CONTAINER_ID } from '../constants' import PluginTypeSwitch from '../plugin-type-switch' +import { HOME_HEADER_HEIGHT_PX } from './home-constants' import { homeCatalogPinnedAtom } from './home-sticky-state' import styles from './home-sticky.module.css' @@ -14,8 +16,6 @@ type HomeCatalogNavigationProps = { catalogTabs: ReactNode } -const STICKY_TOP = 48 - function HomeCatalogNavigation({ catalogCategories, catalogTabs }: HomeCatalogNavigationProps) { const { t } = useTranslation() const isPinned = useAtomValue(homeCatalogPinnedAtom) @@ -23,7 +23,7 @@ function HomeCatalogNavigation({ catalogCategories, catalogTabs }: HomeCatalogNa const pinTriggerRef = useRef(null) useEffect(() => { - const scrollContainer = document.getElementById('marketplace-container') + const scrollContainer = document.getElementById(MARKETPLACE_CONTAINER_ID) if (!scrollContainer) return const previousOverflowAnchor = scrollContainer.style.overflowAnchor @@ -37,7 +37,7 @@ function HomeCatalogNavigation({ catalogCategories, catalogTabs }: HomeCatalogNa const containerTop = scrollContainer.getBoundingClientRect().top const triggerTop = pinTrigger.getBoundingClientRect().top - setIsPinned(triggerTop <= containerTop + STICKY_TOP) + setIsPinned(triggerTop <= containerTop + HOME_HEADER_HEIGHT_PX) } updatePinnedState() @@ -61,6 +61,8 @@ function HomeCatalogNavigation({ catalogCategories, catalogTabs }: HomeCatalogNa styles.catalogNavigation, isPinned && styles.catalogNavigationPinned, )} + // Pins directly below the header, so the offset is the header height. + style={{ top: HOME_HEADER_HEIGHT_PX }} >
{catalogTabs}
diff --git a/web/app/components/plugins/marketplace/home/home-constants.ts b/web/app/components/plugins/marketplace/home/home-constants.ts new file mode 100644 index 00000000000..643a8631cc1 --- /dev/null +++ b/web/app/components/plugins/marketplace/home/home-constants.ts @@ -0,0 +1,6 @@ +/** + * Height of the marketplace home header in pixels. The sticky catalog + * navigation pins itself directly below the header, so the header height and + * the sticky offset must stay in sync; both read this constant. + */ +export const HOME_HEADER_HEIGHT_PX = 48 diff --git a/web/app/components/plugins/marketplace/home/home-header.tsx b/web/app/components/plugins/marketplace/home/home-header.tsx index 874a7fbcf54..d16f8359029 100644 --- a/web/app/components/plugins/marketplace/home/home-header.tsx +++ b/web/app/components/plugins/marketplace/home/home-header.tsx @@ -4,6 +4,7 @@ import Link from '@/next/link' import MarketplaceLogoDark from '@/public/marketplace/dify-marketplace-logo-dark.svg' import MarketplaceLogo from '@/public/marketplace/dify-marketplace-logo.svg' import HomeCatalogTabs from './home-catalog-tabs' +import { HOME_HEADER_HEIGHT_PX } from './home-constants' // HomeCreatorCenter stays in its own client module: it derives styles via // buttonVariants(), which cannot be invoked inside this server component. import HomeCreatorCenter from './home-creator-center' @@ -28,10 +29,8 @@ const HomeHeader = ({ }: HomeHeaderProps) => { return (
+ index * PAGINATION_STEP + (index > selectedIndex ? PAGINATION_ACTIVE_SHIFT : 0) + +type AutoplayPauseReason = 'focus' | 'hover' | 'reduced-motion' | 'user' | 'viewport' | 'visibility' + +function TrendingNavigation({ + banners, + selectedIndex, + carouselRootRef, + pauseWhenOffscreen, + onSelect, + onNext, + onPausedChange, +}: { + banners: PluginBanner[] + selectedIndex: number + carouselRootRef: RefObject + pauseWhenOffscreen: boolean + onSelect: (index: number) => void + onNext: () => void + onPausedChange?: (paused: boolean) => void +}) { + const { t } = useTranslation('plugin') + const progressRef = useRef(null) + const progressAnimationRef = useRef(null) + const pauseReasonsRef = useRef( + new Set(pauseWhenOffscreen ? ['viewport'] : []), + ) + const [isUserPaused, setIsUserPaused] = useState(false) + const [isReducedMotionPaused, setIsReducedMotionPaused] = useState(false) + const isExplicitlyPaused = isUserPaused || isReducedMotionPaused + const paginationWidth = + PAGINATION_ACTIVE_WIDTH + Math.max(0, banners.length - 1) * PAGINATION_STEP + + const setPauseReason = useCallback( + (reason: AutoplayPauseReason, shouldPause: boolean) => { + if (shouldPause) pauseReasonsRef.current.add(reason) + else pauseReasonsRef.current.delete(reason) + + const isPaused = pauseReasonsRef.current.size > 0 + onPausedChange?.(isPaused) + + const progressAnimation = progressAnimationRef.current + if (!progressAnimation) return + + if (isPaused) progressAnimation.pause() + else progressAnimation.play() + }, + [onPausedChange], + ) + + useEffect(() => { + const progressElement = progressRef.current + if (!progressElement?.animate) return + + const progressAnimation = progressElement.animate( + [{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }], + { + duration: AUTOPLAY_DELAY, + easing: 'linear', + fill: 'forwards', + }, + ) + progressAnimationRef.current = progressAnimation + + if (pauseReasonsRef.current.size > 0) progressAnimation.pause() + progressAnimation.onfinish = onNext + + return () => { + progressAnimation.onfinish = null + progressAnimation.cancel() + if (progressAnimationRef.current === progressAnimation) progressAnimationRef.current = null + } + }, [onNext, selectedIndex]) + + useEffect(() => { + const carouselRoot = carouselRootRef.current + if (!carouselRoot) return + + const handleMouseEnter = () => setPauseReason('hover', true) + const handleMouseLeave = () => setPauseReason('hover', false) + const handleFocusIn = () => setPauseReason('focus', true) + const handleFocusOut = (event: FocusEvent) => { + if (carouselRoot.contains(event.relatedTarget as Node | null)) return + setPauseReason('focus', false) + } + const handleVisibilityChange = () => + setPauseReason('visibility', document.visibilityState === 'hidden') + + carouselRoot.addEventListener('mouseenter', handleMouseEnter) + carouselRoot.addEventListener('mouseleave', handleMouseLeave) + carouselRoot.addEventListener('focusin', handleFocusIn) + carouselRoot.addEventListener('focusout', handleFocusOut) + document.addEventListener('visibilitychange', handleVisibilityChange) + handleVisibilityChange() + + return () => { + carouselRoot.removeEventListener('mouseenter', handleMouseEnter) + carouselRoot.removeEventListener('mouseleave', handleMouseLeave) + carouselRoot.removeEventListener('focusin', handleFocusIn) + carouselRoot.removeEventListener('focusout', handleFocusOut) + document.removeEventListener('visibilitychange', handleVisibilityChange) + } + }, [carouselRootRef, setPauseReason]) + + useEffect(() => { + if (!pauseWhenOffscreen) { + setPauseReason('viewport', false) + return + } + + const carouselRoot = carouselRootRef.current + if (!carouselRoot) return + + if (typeof IntersectionObserver === 'undefined') { + setPauseReason('viewport', false) + return + } + + const observer = new IntersectionObserver( + ([entry]) => { + const isVisible = !!entry?.isIntersecting && entry.intersectionRatio >= 0.25 + setPauseReason('viewport', !isVisible) + }, + { + root: document.getElementById(MARKETPLACE_CONTAINER_ID), + threshold: 0.25, + }, + ) + + observer.observe(carouselRoot) + + return () => observer.disconnect() + }, [carouselRootRef, pauseWhenOffscreen, setPauseReason]) + + useEffect(() => { + const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)') + const syncReducedMotion = () => { + // oxlint-disable-next-line eslint-react/set-state-in-effect -- This state mirrors an external media query. + setIsReducedMotionPaused(reducedMotionQuery.matches) + setPauseReason('reduced-motion', reducedMotionQuery.matches) + } + + syncReducedMotion() + reducedMotionQuery.addEventListener('change', syncReducedMotion) + + return () => reducedMotionQuery.removeEventListener('change', syncReducedMotion) + }, [setPauseReason]) + + const toggleAutoplay = () => { + if (isExplicitlyPaused) { + setIsUserPaused(false) + setIsReducedMotionPaused(false) + setPauseReason('user', false) + setPauseReason('reduced-motion', false) + // Activating Play keeps the pointer and/or keyboard focus on the button + // itself, so the implicit hover/focus reasons would silently keep the + // rotation paused. An explicit Play overrides them; they re-engage on + // the next mouseenter/focusin. + setPauseReason('focus', false) + setPauseReason('hover', false) + return + } + + setIsUserPaused(true) + setPauseReason('user', true) + } + + return ( +
$['marketplace.home.trendingPaginationLabel'])} + className={cn( + styles.navigation, + 'absolute right-0 z-10 flex h-[22px] items-center gap-2 px-5 py-2', + )} + > +
+ + + + {banners.map((banner, index) => { + const isCurrent = index === selectedIndex + + return ( +
+
+ +
+ ) +} + +export default TrendingNavigation diff --git a/web/app/components/plugins/marketplace/home/home-trending-slides.tsx b/web/app/components/plugins/marketplace/home/home-trending-slides.tsx new file mode 100644 index 00000000000..49e78026644 --- /dev/null +++ b/web/app/components/plugins/marketplace/home/home-trending-slides.tsx @@ -0,0 +1,331 @@ +'use client' + +import type { + BannerAd, + BannerBlog, + BannerEvent, + BannerRecommend, + BannerRecommendCard, + PluginBanner, +} from '@dify/contracts/marketplace' +import { cn } from '@langgenius/dify-ui/cn' +import { useTranslation } from '#i18n' +import Partner from '@/app/components/plugins/base/badges/partner' +import Verified from '@/app/components/plugins/base/badges/verified' +import { MARKETPLACE_API_PREFIX } from '@/config' +import Link from '@/next/link' +import { getPluginLinkInMarketplace } from '../utils' +import background from './assets/background.webp' +import difyUpdatesArt from './assets/dify-updates-art.png' +import styles from './home-trending.module.css' + +const getMarketplaceAssetURL = (path?: string) => { + if (!path) return '' + if (/^https?:\/\//.test(path) || path.startsWith('/_next/')) 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) return getLocalCardHref(card) + if (card.link) return card.link + + // The embedded console has no local plugin detail route, so a plugin card + // without a delivery-provided link opens the marketplace site detail page. + if (card.item_type === 'plugin') { + const [organization, pluginName] = card.item_id.split('/') + if (organization && pluginName) + return getPluginLinkInMarketplace({ org: organization, name: pluginName, type: 'plugin' }) + } + + return getLocalCardHref(card) +} + +const getCardCreator = (card: BannerRecommendCard) => { + if (card.creator) return card.creator + if (card.item_type !== 'plugin') return '' + + return card.item_id.split('/')[0] || '' +} + +function TrendingCopy({ + banner, + isMarketplacePlatform, +}: { + banner: BannerRecommend + isMarketplacePlatform: boolean +}) { + const { t } = useTranslation('plugin') + const heading = banner.content.heading || t(($) => $['marketplace.home.trendingTitle']) + const description = + banner.content.description || + banner.content.subheadings?.join(' · ') || + t(($) => $['marketplace.home.trendingDescription']) + + return ( +
+
+

+ {banner.title} +

+

+ {heading} +

+

+ {description} +

+
+
+ ) +} + +function TrendingCard({ + card, + isMarketplacePlatform, +}: { + card: BannerRecommendCard + isMarketplacePlatform: boolean +}) { + 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) + const isPartner = card.badges?.includes('partner') + const isVerified = card.badges?.includes('verified') + + return ( + +
+ {iconURL ? ( + + ) : card.icon ? ( + {card.icon} + ) : ( +
+ +
+
+
+

+ {card.display_name} +

+ {(isPartner || isVerified) && ( +
+ {isPartner && ( + $['marketplace.partnerTip'])} /> + )} + {isVerified && ( + $['marketplace.verifiedTip'])} /> + )} +
+ )} +
+ {creator && ( +

+ {t(($) => $['marketplace.home.trendingByCreator'], { creator })} +

+ )} +
+ + {t(($) => $['marketplace.home.trendingView'])} + +
+ + ) +} + +function TrendingRecommendationSlide({ + banner, + isMarketplacePlatform, +}: { + banner: BannerRecommend + isMarketplacePlatform: boolean +}) { + return ( +
+ +
+ +
+ +
+ {banner.content.cards.map((card) => ( + + ))} +
+
+
+ ) +} + +function BlogBannerSlide({ banner }: { banner: BannerBlog }) { + const { t } = useTranslation('plugin') + const opensInNewTab = /^https?:\/\//.test(banner.content.link) + + return ( + $['marketplace.home.trendingReadMoreAbout'], { + title: banner.content.blog_title, + })} + className="flex h-[200px] w-full overflow-hidden rounded-2xl bg-background-body outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid" + > +
+
+

+ {banner.title} +

+
+

+ {banner.content.blog_title} +

+
+ {banner.content.subtitle && ( +

+ {banner.content.subtitle} +

+ )} + {banner.content.description && ( +

+ {banner.content.description} +

+ )} + + {t(($) => $['marketplace.home.trendingReadMore'])} + + +
+
+
+
+ + + ) +} + +function ImageBannerSlide({ banner }: { banner: BannerEvent | BannerAd }) { + const desktopImage = getMarketplaceAssetURL(banner.content.images.desktop) + const tabletImage = getMarketplaceAssetURL(banner.content.images.tablet) + const mobileImage = getMarketplaceAssetURL(banner.content.images.mobile) + + return ( + + + {mobileImage && } + {tabletImage && } + + + + ) +} + +export function HomeBannerSlide({ + banner, + isMarketplacePlatform, +}: { + banner: PluginBanner + isMarketplacePlatform: boolean +}) { + if (banner.style_type === 'blog') return + + if (banner.style_type === 'event' || banner.style_type === 'ad') + return + + return ( + + ) +} diff --git a/web/app/components/plugins/marketplace/home/home-trending.tsx b/web/app/components/plugins/marketplace/home/home-trending.tsx index dca809ba9dc..d55e56243fd 100644 --- a/web/app/components/plugins/marketplace/home/home-trending.tsx +++ b/web/app/components/plugins/marketplace/home/home-trending.tsx @@ -1,574 +1,12 @@ 'use client' -import type { - BannerAd, - BannerBlog, - BannerEvent, - BannerRecommend, - BannerRecommendCard, - PluginBanner, -} from '@dify/contracts/marketplace' -import type { RefObject } from 'react' +import type { PluginBanner } from '@dify/contracts/marketplace' import { cn } from '@langgenius/dify-ui/cn' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { useTranslation } from '#i18n' -import Partner from '@/app/components/plugins/base/badges/partner' -import Verified from '@/app/components/plugins/base/badges/verified' -import { MARKETPLACE_API_PREFIX } from '@/config' -import Link from '@/next/link' -import background from './assets/background.webp' -import difyUpdatesArt from './assets/dify-updates-art.png' import styles from './home-trending.module.css' - -const AUTOPLAY_DELAY = 5000 -const PAGINATION_DOT_SIZE = 6 -const PAGINATION_ACTIVE_WIDTH = 40 -const PAGINATION_GAP = 8 -const PAGINATION_STEP = PAGINATION_DOT_SIZE + PAGINATION_GAP -const PAGINATION_ACTIVE_SHIFT = PAGINATION_ACTIVE_WIDTH - PAGINATION_DOT_SIZE - -const getPaginationItemOffset = (index: number, selectedIndex: number) => - index * PAGINATION_STEP + (index > selectedIndex ? PAGINATION_ACTIVE_SHIFT : 0) - -type AutoplayPauseReason = 'focus' | 'hover' | 'reduced-motion' | 'user' | 'viewport' | 'visibility' - -function TrendingCopy({ - banner, - isMarketplacePlatform, -}: { - banner: BannerRecommend - isMarketplacePlatform: boolean -}) { - const { t } = useTranslation('plugin') - const heading = banner.content.heading || t(($) => $['marketplace.home.trendingTitle']) - const description = - banner.content.description || - banner.content.subheadings?.join(' · ') || - t(($) => $['marketplace.home.trendingDescription']) - - return ( -
-
-

- {banner.title} -

-

- {heading} -

-

- {description} -

-
-
- ) -} - -const getMarketplaceAssetURL = (path?: string) => { - if (!path) return '' - if (/^https?:\/\//.test(path) || path.startsWith('/_next/')) 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.creator) return card.creator - if (card.item_type !== 'plugin') return '' - - return card.item_id.split('/')[0] || '' -} - -function TrendingCard({ - card, - isMarketplacePlatform, -}: { - card: BannerRecommendCard - isMarketplacePlatform: boolean -}) { - 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) - const isPartner = card.badges?.includes('partner') - const isVerified = card.badges?.includes('verified') - - return ( - -
- {iconURL ? ( - - ) : card.icon ? ( - {card.icon} - ) : ( -
- -
-
-
-

- {card.display_name} -

- {(isPartner || isVerified) && ( -
- {isPartner && ( - $['marketplace.partnerTip'])} /> - )} - {isVerified && ( - $['marketplace.verifiedTip'])} /> - )} -
- )} -
- {creator && ( -

- {t(($) => $['marketplace.home.trendingByCreator'], { creator })} -

- )} -
- - {t(($) => $['marketplace.home.trendingView'])} - -
- - ) -} - -function TrendingRecommendationSlide({ - banner, - isMarketplacePlatform, -}: { - banner: BannerRecommend - isMarketplacePlatform: boolean -}) { - return ( -
- -
- -
- -
- {banner.content.cards.map((card) => ( - - ))} -
-
-
- ) -} - -function BlogBannerSlide({ banner }: { banner: BannerBlog }) { - const { t } = useTranslation('plugin') - const opensInNewTab = /^https?:\/\//.test(banner.content.link) - - return ( - $['marketplace.home.trendingReadMoreAbout'], { - title: banner.content.blog_title, - })} - className="flex h-[200px] w-full overflow-hidden rounded-2xl bg-background-body outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid" - > -
-
-

- {banner.title} -

-
-

- {banner.content.blog_title} -

-
- {banner.content.subtitle && ( -

- {banner.content.subtitle} -

- )} - {banner.content.description && ( -

- {banner.content.description} -

- )} - - {t(($) => $['marketplace.home.trendingReadMore'])} - - -
-
-
-
- - - ) -} - -function ImageBannerSlide({ banner }: { banner: BannerEvent | BannerAd }) { - const desktopImage = getMarketplaceAssetURL(banner.content.images.desktop) - const tabletImage = getMarketplaceAssetURL(banner.content.images.tablet) - const mobileImage = getMarketplaceAssetURL(banner.content.images.mobile) - - return ( - - - {mobileImage && } - {tabletImage && } - - - - ) -} - -function HomeBannerSlide({ - banner, - isMarketplacePlatform, -}: { - banner: PluginBanner - isMarketplacePlatform: boolean -}) { - if (banner.style_type === 'blog') return - - if (banner.style_type === 'event' || banner.style_type === 'ad') - return - - return ( - - ) -} - -function TrendingNavigation({ - banners, - selectedIndex, - carouselRootRef, - pauseWhenOffscreen, - onSelect, - onNext, - onPausedChange, -}: { - banners: PluginBanner[] - selectedIndex: number - carouselRootRef: RefObject - pauseWhenOffscreen: boolean - onSelect: (index: number) => void - onNext: () => void - onPausedChange?: (paused: boolean) => void -}) { - const { t } = useTranslation('plugin') - const progressRef = useRef(null) - const progressAnimationRef = useRef(null) - const pauseReasonsRef = useRef( - new Set(pauseWhenOffscreen ? ['viewport'] : []), - ) - const [isUserPaused, setIsUserPaused] = useState(false) - const [isReducedMotionPaused, setIsReducedMotionPaused] = useState(false) - const isExplicitlyPaused = isUserPaused || isReducedMotionPaused - const paginationWidth = - PAGINATION_ACTIVE_WIDTH + Math.max(0, banners.length - 1) * PAGINATION_STEP - - const setPauseReason = useCallback( - (reason: AutoplayPauseReason, shouldPause: boolean) => { - if (shouldPause) pauseReasonsRef.current.add(reason) - else pauseReasonsRef.current.delete(reason) - - const isPaused = pauseReasonsRef.current.size > 0 - onPausedChange?.(isPaused) - - const progressAnimation = progressAnimationRef.current - if (!progressAnimation) return - - if (isPaused) progressAnimation.pause() - else progressAnimation.play() - }, - [onPausedChange], - ) - - useEffect(() => { - const progressElement = progressRef.current - if (!progressElement?.animate) return - - const progressAnimation = progressElement.animate( - [{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }], - { - duration: AUTOPLAY_DELAY, - easing: 'linear', - fill: 'forwards', - }, - ) - progressAnimationRef.current = progressAnimation - - if (pauseReasonsRef.current.size > 0) progressAnimation.pause() - progressAnimation.onfinish = onNext - - return () => { - progressAnimation.onfinish = null - progressAnimation.cancel() - if (progressAnimationRef.current === progressAnimation) progressAnimationRef.current = null - } - }, [onNext, selectedIndex]) - - useEffect(() => { - const carouselRoot = carouselRootRef.current - if (!carouselRoot) return - - const handleMouseEnter = () => setPauseReason('hover', true) - const handleMouseLeave = () => setPauseReason('hover', false) - const handleFocusIn = () => setPauseReason('focus', true) - const handleFocusOut = (event: FocusEvent) => { - if (carouselRoot.contains(event.relatedTarget as Node | null)) return - setPauseReason('focus', false) - } - const handleVisibilityChange = () => - setPauseReason('visibility', document.visibilityState === 'hidden') - - carouselRoot.addEventListener('mouseenter', handleMouseEnter) - carouselRoot.addEventListener('mouseleave', handleMouseLeave) - carouselRoot.addEventListener('focusin', handleFocusIn) - carouselRoot.addEventListener('focusout', handleFocusOut) - document.addEventListener('visibilitychange', handleVisibilityChange) - handleVisibilityChange() - - return () => { - carouselRoot.removeEventListener('mouseenter', handleMouseEnter) - carouselRoot.removeEventListener('mouseleave', handleMouseLeave) - carouselRoot.removeEventListener('focusin', handleFocusIn) - carouselRoot.removeEventListener('focusout', handleFocusOut) - document.removeEventListener('visibilitychange', handleVisibilityChange) - } - }, [carouselRootRef, setPauseReason]) - - useEffect(() => { - if (!pauseWhenOffscreen) { - setPauseReason('viewport', false) - return - } - - const carouselRoot = carouselRootRef.current - if (!carouselRoot) return - - if (typeof IntersectionObserver === 'undefined') { - setPauseReason('viewport', false) - return - } - - const observer = new IntersectionObserver( - ([entry]) => { - const isVisible = !!entry?.isIntersecting && entry.intersectionRatio >= 0.25 - setPauseReason('viewport', !isVisible) - }, - { - root: document.getElementById('marketplace-container'), - threshold: 0.25, - }, - ) - - observer.observe(carouselRoot) - - return () => observer.disconnect() - }, [carouselRootRef, pauseWhenOffscreen, setPauseReason]) - - useEffect(() => { - const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)') - const syncReducedMotion = () => { - // oxlint-disable-next-line eslint-react/set-state-in-effect -- This state mirrors an external media query. - setIsReducedMotionPaused(reducedMotionQuery.matches) - setPauseReason('reduced-motion', reducedMotionQuery.matches) - } - - syncReducedMotion() - reducedMotionQuery.addEventListener('change', syncReducedMotion) - - return () => reducedMotionQuery.removeEventListener('change', syncReducedMotion) - }, [setPauseReason]) - - const toggleAutoplay = () => { - if (isExplicitlyPaused) { - setIsUserPaused(false) - setIsReducedMotionPaused(false) - setPauseReason('user', false) - setPauseReason('reduced-motion', false) - // Activating Play keeps the pointer and/or keyboard focus on the button - // itself, so the implicit hover/focus reasons would silently keep the - // rotation paused. An explicit Play overrides them; they re-engage on - // the next mouseenter/focusin. - setPauseReason('focus', false) - setPauseReason('hover', false) - return - } - - setIsUserPaused(true) - setPauseReason('user', true) - } - - return ( -
$['marketplace.home.trendingPaginationLabel'])} - className={cn( - styles.navigation, - 'absolute right-0 z-10 flex h-[22px] items-center gap-2 px-5 py-2', - )} - > -
- - - - {banners.map((banner, index) => { - const isCurrent = index === selectedIndex - - return ( -
-
- -
- ) -} +import TrendingNavigation from './home-trending-navigation' +import { HomeBannerSlide } from './home-trending-slides' function HomeTrending({ banners, diff --git a/web/app/components/plugins/marketplace/hooks.ts b/web/app/components/plugins/marketplace/hooks.ts index 78f8d19ccbe..cf884c3d74b 100644 --- a/web/app/components/plugins/marketplace/hooks.ts +++ b/web/app/components/plugins/marketplace/hooks.ts @@ -207,7 +207,7 @@ export const useMarketplacePlugins = (enabled = true) => { export const useMarketplaceContainerScroll = ( callback: () => void, - scrollContainerId = 'marketplace-container', + scrollContainerId = MARKETPLACE_CONTAINER_ID, ) => { const handleScroll = useCallback( (e: Event) => { diff --git a/web/app/components/plugins/marketplace/list/carousel.tsx b/web/app/components/plugins/marketplace/list/carousel.tsx index 5326b4b7565..491e02b4726 100644 --- a/web/app/components/plugins/marketplace/list/carousel.tsx +++ b/web/app/components/plugins/marketplace/list/carousel.tsx @@ -7,6 +7,7 @@ import Autoplay from 'embla-carousel-autoplay' import useEmblaCarousel from 'embla-carousel-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from '#i18n' +import { MARKETPLACE_CONTAINER_ID } from '../constants' import { CAROUSEL_PAGE_CLASS } from './collection-constants' export type CarouselPage = { @@ -314,7 +315,7 @@ const Carousel = ({ syncAutoplay() }, { - root: document.getElementById('marketplace-container'), + root: document.getElementById(MARKETPLACE_CONTAINER_ID), threshold: 0.25, }, ) diff --git a/web/app/components/plugins/marketplace/list/collection-constants.ts b/web/app/components/plugins/marketplace/list/collection-constants.ts index 842d9acb785..6f8edf35ab4 100644 --- a/web/app/components/plugins/marketplace/list/collection-constants.ts +++ b/web/app/components/plugins/marketplace/list/collection-constants.ts @@ -1,5 +1,11 @@ export const GRID_CLASS = 'grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4' +export const BECOME_PARTNER_URL = 'https://share-na2.hsforms.com/1NiS4r9lsSqGcuNBB77DeEQ40s9fk' + +// Collections whose header shows the "Become a Partner" call to action, as +// named by the Marketplace API for the plugin and template catalogs. +export const PARTNER_COLLECTION_NAMES = new Set(['partners', 'partner-template', 'Partner Template']) + export const CAROUSEL_PAGE_CLASS = 'w-full shrink-0' export const CAROUSEL_PAGE_SIZE = { diff --git a/web/app/components/plugins/marketplace/list/list-with-collection.tsx b/web/app/components/plugins/marketplace/list/list-with-collection.tsx index 8f7fd0d02a8..de9a61bffe5 100644 --- a/web/app/components/plugins/marketplace/list/list-with-collection.tsx +++ b/web/app/components/plugins/marketplace/list/list-with-collection.tsx @@ -7,14 +7,13 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useLocale, useTranslation } from '#i18n' import { getLanguage } from '@/i18n-config/language' import { useMarketplaceMoreClick } from '../atoms' +import { MARKETPLACE_CONTAINER_ID } from '../constants' import { buildCarouselPages } from '../utils' import CardWrapper from './card-wrapper' import Carousel from './carousel' -import { GRID_CLASS } from './collection-constants' +import { BECOME_PARTNER_URL, GRID_CLASS, PARTNER_COLLECTION_NAMES } from './collection-constants' import { useCarouselItemsPerPage } from './use-carousel-items-per-page' -const BECOME_PARTNER_URL = 'https://share-na2.hsforms.com/1NiS4r9lsSqGcuNBB77DeEQ40s9fk' -const PARTNERS_COLLECTION_NAMES = new Set(['partners', 'partner-template', 'Partner Template']) const COLLECTION_PRELOAD_MARGIN = '320px 0px' const COLLECTION_INTERSECTION_THRESHOLD = 0.01 const MAX_PLACEHOLDER_CARDS = 8 @@ -110,7 +109,7 @@ const CollectionSection = ({ const [isMounted, setIsMounted] = useState(!deferMount) const pages = useMemo(() => buildCarouselPages(plugins, itemsPerPage), [itemsPerPage, plugins]) const hasMultiplePages = pages.length > 1 - const isPartnersCollection = PARTNERS_COLLECTION_NAMES.has(collection.name) + const isPartnersCollection = PARTNER_COLLECTION_NAMES.has(collection.name) useEffect(() => { if (!deferMount || isMounted) return @@ -132,7 +131,7 @@ const CollectionSection = ({ observer.disconnect() }, { - root: document.getElementById('marketplace-container'), + root: document.getElementById(MARKETPLACE_CONTAINER_ID), rootMargin: COLLECTION_PRELOAD_MARGIN, threshold: COLLECTION_INTERSECTION_THRESHOLD, }, diff --git a/web/i18n/ar-TN/plugin.json b/web/i18n/ar-TN/plugin.json index 678fb890316..9bcf5ccf1fc 100644 --- a/web/i18n/ar-TN/plugin.json +++ b/web/i18n/ar-TN/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "دليل", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "اكتشف. وسّع. ابنِ", - "marketplace.home.new": "جديد", "marketplace.home.plugins": "المكونات الإضافية", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.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.trendingPause": "إيقاف مؤقت", "marketplace.home.trendingPlay": "تشغيل", diff --git a/web/i18n/de-DE/plugin.json b/web/i18n/de-DE/plugin.json index fb787b3bee3..8c20ddd3a4c 100644 --- a/web/i18n/de-DE/plugin.json +++ b/web/i18n/de-DE/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Leitfaden", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Entdecken. Erweitern. Entwickeln", - "marketplace.home.new": "Neu", "marketplace.home.plugins": "Plugins", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Vorlagen", "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.trendingPause": "Pausieren", "marketplace.home.trendingPlay": "Abspielen", diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json index e21f46e7436..7d061349379 100644 --- a/web/i18n/en-US/plugin.json +++ b/web/i18n/en-US/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Guide", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Discover. Extend. Build", - "marketplace.home.new": "New", "marketplace.home.plugins": "Plugins", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "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.trendingPause": "Pause", "marketplace.home.trendingPlay": "Play", diff --git a/web/i18n/es-ES/plugin.json b/web/i18n/es-ES/plugin.json index caf07c5a954..3d638aa00be 100644 --- a/web/i18n/es-ES/plugin.json +++ b/web/i18n/es-ES/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Guía", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Descubre. Amplía. Crea", - "marketplace.home.new": "Nuevo", "marketplace.home.plugins": "Plugins", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Plantillas", "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.trendingPause": "Pausar", "marketplace.home.trendingPlay": "Reproducir", diff --git a/web/i18n/fa-IR/plugin.json b/web/i18n/fa-IR/plugin.json index 87a6c5bbea2..c9b98db4253 100644 --- a/web/i18n/fa-IR/plugin.json +++ b/web/i18n/fa-IR/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "راهنما", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "کشف کنید. گسترش دهید. بسازید", - "marketplace.home.new": "جدید", "marketplace.home.plugins": "افزونه‌ها", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.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.trendingPause": "توقف", "marketplace.home.trendingPlay": "پخش", diff --git a/web/i18n/fr-FR/plugin.json b/web/i18n/fr-FR/plugin.json index 98ca0e05c76..9f4192355fd 100644 --- a/web/i18n/fr-FR/plugin.json +++ b/web/i18n/fr-FR/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Guide", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Découvrez. Étendez. Créez", - "marketplace.home.new": "Nouveau", "marketplace.home.plugins": "Plugins", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Modèles", "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.trendingPause": "Mettre en pause", "marketplace.home.trendingPlay": "Lire", diff --git a/web/i18n/hi-IN/plugin.json b/web/i18n/hi-IN/plugin.json index fe39344105c..36698cdb50c 100644 --- a/web/i18n/hi-IN/plugin.json +++ b/web/i18n/hi-IN/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "मार्गदर्शिका", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "खोजें। विस्तार करें। बनाएँ", - "marketplace.home.new": "नया", "marketplace.home.plugins": "एकीकरण", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.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.trendingPause": "रोकें", "marketplace.home.trendingPlay": "चलाएं", diff --git a/web/i18n/id-ID/plugin.json b/web/i18n/id-ID/plugin.json index ded124aaa0b..69ab35c7abb 100644 --- a/web/i18n/id-ID/plugin.json +++ b/web/i18n/id-ID/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Panduan", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Temukan. Perluas. Bangun", - "marketplace.home.new": "Baru", "marketplace.home.plugins": "Plugin", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Templat", "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.trendingPause": "Jeda", "marketplace.home.trendingPlay": "Putar", diff --git a/web/i18n/it-IT/plugin.json b/web/i18n/it-IT/plugin.json index 46cfa83f3f4..31e301be272 100644 --- a/web/i18n/it-IT/plugin.json +++ b/web/i18n/it-IT/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Guida", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Scopri. Estendi. Crea", - "marketplace.home.new": "Nuovo", "marketplace.home.plugins": "Plugin", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Modelli", "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.trendingPause": "Pausa", "marketplace.home.trendingPlay": "Riproduci", diff --git a/web/i18n/ja-JP/plugin.json b/web/i18n/ja-JP/plugin.json index 882bd291666..5e380c42c2d 100644 --- a/web/i18n/ja-JP/plugin.json +++ b/web/i18n/ja-JP/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "ガイド", "marketplace.home.heroSubtitle": "Dify Marketplace で、より安全で信頼性の高いプラグインを見つけましょう。", "marketplace.home.heroTitle": "見つける。拡張する。構築する", - "marketplace.home.new": "新着", "marketplace.home.plugins": "プラグイン", "marketplace.home.searchPlaceholder": "プラグインまたはテンプレートを検索", "marketplace.home.templates": "テンプレート", "marketplace.home.trendingByCreator": "{{creator}} 作成", "marketplace.home.trendingDescription": "実際の利用状況に基づく人気プラグインを2週間ごとに更新。ワークスペースでの実行数によるランキングで、有料掲載や編集部による選定はありません。", - "marketplace.home.trendingEyebrow": "トレンド", "marketplace.home.trendingPaginationLabel": "トレンドページ", "marketplace.home.trendingPause": "一時停止", "marketplace.home.trendingPlay": "再生", diff --git a/web/i18n/ko-KR/plugin.json b/web/i18n/ko-KR/plugin.json index fba41c7722b..592513cf3f6 100644 --- a/web/i18n/ko-KR/plugin.json +++ b/web/i18n/ko-KR/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "가이드", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "발견하고, 확장하고, 구축하세요", - "marketplace.home.new": "신규", "marketplace.home.plugins": "플러그인", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.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.trendingPause": "일시정지", "marketplace.home.trendingPlay": "재생", diff --git a/web/i18n/lo-LA/plugin.json b/web/i18n/lo-LA/plugin.json index f95fc19f64e..2b227df973e 100644 --- a/web/i18n/lo-LA/plugin.json +++ b/web/i18n/lo-LA/plugin.json @@ -231,6 +231,15 @@ "marketplace.discover": "ຄົ້ນຫາ", "marketplace.empower": "ເສີມພະລັງການພັດທະນາ AI ຂອງທ່ານ", "marketplace.home.creatorCenter": "ສູນຜູ້ສ້າງ", + "marketplace.home.guide": "ຄູ່ມື", + "marketplace.home.heroSubtitle": "ສ້າງດ້ວຍປລັກອິນທີ່ປອດໄພ ແລະ ເຊື່ອຖືໄດ້ຫຼາຍຂຶ້ນຈາກ Dify Marketplace.", + "marketplace.home.heroTitle": "ຄົ້ນຫາ. ຕໍ່ຍອດ. ສ້າງສັນ", + "marketplace.home.plugins": "ປລັກອິນ", + "marketplace.home.searchPlaceholder": "ຄົ້ນຫາປລັກອິນ ຫຼື ແມ່ແບບ", + "marketplace.home.templates": "ແມ່ແບບ", + "marketplace.home.trendingByCreator": "ໂດຍ {{creator}}", + "marketplace.home.trendingDescription": "ຄັດເລືອກຈາກການນຳໃຊ້ຕົວຈິງ, ອັບເດດທຸກໆສອງອາທິດ. ຈັດອັນດັບຕາມການເອີ້ນໃຊ້ຕົວຈິງໃນທົ່ວທຸກ workspace — ບໍ່ມີການຈ່າຍເງິນເພື່ອໂຄສະນາ ຫຼື ການຄັດເລືອກໂດຍທີມງານ.", + "marketplace.home.trendingPaginationLabel": "ໜ້າກຳລັງນິຍົມ", "marketplace.home.trendingPause": "ຢຸດຊົ່ວຄາວ", "marketplace.home.trendingPlay": "ຫຼິ້ນ", "marketplace.home.trendingReadMore": "ອ່ານເພີ່ມເຕີມ", diff --git a/web/i18n/nl-NL/plugin.json b/web/i18n/nl-NL/plugin.json index 39978996248..5fa46b37dcf 100644 --- a/web/i18n/nl-NL/plugin.json +++ b/web/i18n/nl-NL/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Handleiding", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Ontdek. Breid uit. Bouw", - "marketplace.home.new": "Nieuw", "marketplace.home.plugins": "Plugins", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Sjablonen", "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.trendingPause": "Pauzeren", "marketplace.home.trendingPlay": "Afspelen", diff --git a/web/i18n/pl-PL/plugin.json b/web/i18n/pl-PL/plugin.json index 6654f9591f4..7861f70a33b 100644 --- a/web/i18n/pl-PL/plugin.json +++ b/web/i18n/pl-PL/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Przewodnik", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Odkrywaj. Rozszerzaj. Twórz", - "marketplace.home.new": "Nowość", "marketplace.home.plugins": "Integracje", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Szablony", "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.trendingPause": "Wstrzymaj", "marketplace.home.trendingPlay": "Odtwórz", diff --git a/web/i18n/pt-BR/plugin.json b/web/i18n/pt-BR/plugin.json index e3e3ad7898d..4be9f3eaf0e 100644 --- a/web/i18n/pt-BR/plugin.json +++ b/web/i18n/pt-BR/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Guia", "marketplace.home.heroSubtitle": "Crie com plugins mais seguros e confiáveis do Dify Marketplace.", "marketplace.home.heroTitle": "Descubra. Expanda. Crie", - "marketplace.home.new": "Novo", "marketplace.home.plugins": "Plugins", "marketplace.home.searchPlaceholder": "Buscar plugins ou modelos", "marketplace.home.templates": "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.trendingPause": "Pausar", "marketplace.home.trendingPlay": "Reproduzir", diff --git a/web/i18n/ro-RO/plugin.json b/web/i18n/ro-RO/plugin.json index 16e9c7d0840..292c356f596 100644 --- a/web/i18n/ro-RO/plugin.json +++ b/web/i18n/ro-RO/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Ghid", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Descoperă. Extinde. Construiește", - "marketplace.home.new": "Nou", "marketplace.home.plugins": "Plugin-uri", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Șabloane", "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.trendingPause": "Pauză", "marketplace.home.trendingPlay": "Redare", diff --git a/web/i18n/ru-RU/plugin.json b/web/i18n/ru-RU/plugin.json index fc052a2ede5..61044f78f79 100644 --- a/web/i18n/ru-RU/plugin.json +++ b/web/i18n/ru-RU/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Руководство", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Открывайте. Расширяйте. Создавайте", - "marketplace.home.new": "Новое", "marketplace.home.plugins": "Интеграции", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.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.trendingPause": "Пауза", "marketplace.home.trendingPlay": "Воспроизвести", diff --git a/web/i18n/sl-SI/plugin.json b/web/i18n/sl-SI/plugin.json index 9ec898c50d5..8f2365b14ca 100644 --- a/web/i18n/sl-SI/plugin.json +++ b/web/i18n/sl-SI/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Vodnik", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Odkrijte. Razširite. Ustvarite", - "marketplace.home.new": "Novo", "marketplace.home.plugins": "Integracije", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Predloge", "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.trendingPause": "Premor", "marketplace.home.trendingPlay": "Predvajaj", diff --git a/web/i18n/th-TH/plugin.json b/web/i18n/th-TH/plugin.json index 8432d45ed54..2a880f6653f 100644 --- a/web/i18n/th-TH/plugin.json +++ b/web/i18n/th-TH/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "คู่มือ", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "ค้นพบ ขยาย และสร้าง", - "marketplace.home.new": "ใหม่", "marketplace.home.plugins": "ปลั๊กอิน", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.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.trendingPause": "หยุดชั่วคราว", "marketplace.home.trendingPlay": "เล่น", diff --git a/web/i18n/tr-TR/plugin.json b/web/i18n/tr-TR/plugin.json index 201db3822a8..06a1495afc2 100644 --- a/web/i18n/tr-TR/plugin.json +++ b/web/i18n/tr-TR/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Kılavuz", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Keşfet. Genişlet. Oluştur", - "marketplace.home.new": "Yeni", "marketplace.home.plugins": "Eklentiler", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Şablonlar", "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.trendingPause": "Duraklat", "marketplace.home.trendingPlay": "Oynat", diff --git a/web/i18n/uk-UA/plugin.json b/web/i18n/uk-UA/plugin.json index a5603cdc961..beeb31ed725 100644 --- a/web/i18n/uk-UA/plugin.json +++ b/web/i18n/uk-UA/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Посібник", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Відкривайте. Розширюйте. Створюйте", - "marketplace.home.new": "Нове", "marketplace.home.plugins": "Інтеграції", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.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.trendingPause": "Пауза", "marketplace.home.trendingPlay": "Відтворити", diff --git a/web/i18n/vi-VN/plugin.json b/web/i18n/vi-VN/plugin.json index 9eb1f230177..3fab1ba90ca 100644 --- a/web/i18n/vi-VN/plugin.json +++ b/web/i18n/vi-VN/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "Hướng dẫn", "marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.", "marketplace.home.heroTitle": "Khám phá. Mở rộng. Xây dựng", - "marketplace.home.new": "Mới", "marketplace.home.plugins": "Plugin", "marketplace.home.searchPlaceholder": "Search plugins or templates", "marketplace.home.templates": "Mẫu", "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.trendingPause": "Tạm dừng", "marketplace.home.trendingPlay": "Phát", diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json index 44613f2f189..6c08070d007 100644 --- a/web/i18n/zh-Hans/plugin.json +++ b/web/i18n/zh-Hans/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "指南", "marketplace.home.heroSubtitle": "在 Dify Marketplace 探索更安全、更可靠的插件。", "marketplace.home.heroTitle": "发现。扩展。构建", - "marketplace.home.new": "新", "marketplace.home.plugins": "插件", "marketplace.home.searchPlaceholder": "搜索插件或模板", "marketplace.home.templates": "模板", "marketplace.home.trendingByCreator": "由 {{creator}} 发布", "marketplace.home.trendingDescription": "基于真实使用情况选出的热门插件,每两周更新一次。榜单按各工作区的实际运行次数排序,不含付费推广或编辑推荐。", - "marketplace.home.trendingEyebrow": "当前热门", "marketplace.home.trendingPaginationLabel": "热门推荐页码", "marketplace.home.trendingPause": "暂停", "marketplace.home.trendingPlay": "播放", diff --git a/web/i18n/zh-Hant/plugin.json b/web/i18n/zh-Hant/plugin.json index 5bbb84e2d9b..6ea91394ca3 100644 --- a/web/i18n/zh-Hant/plugin.json +++ b/web/i18n/zh-Hant/plugin.json @@ -234,13 +234,11 @@ "marketplace.home.guide": "指南", "marketplace.home.heroSubtitle": "在 Dify Marketplace 探索更安全、更可靠的外掛程式。", "marketplace.home.heroTitle": "探索。擴展。建構", - "marketplace.home.new": "新", "marketplace.home.plugins": "外掛", "marketplace.home.searchPlaceholder": "搜尋外掛程式或範本", "marketplace.home.templates": "範本", "marketplace.home.trendingByCreator": "由 {{creator}} 發布", "marketplace.home.trendingDescription": "根據真實使用情況選出的熱門外掛程式,每兩週更新一次。榜單按各工作區的實際執行次數排序,不含付費推廣或編輯推薦。", - "marketplace.home.trendingEyebrow": "目前熱門", "marketplace.home.trendingPaginationLabel": "熱門推薦頁碼", "marketplace.home.trendingPause": "暫停", "marketplace.home.trendingPlay": "播放",