diff --git a/apps/www/app/partners/integrations/IntegrationsContent.tsx b/apps/www/app/partners/integrations/IntegrationsContent.tsx index f6ce38b3544..2ea02a76298 100644 --- a/apps/www/app/partners/integrations/IntegrationsContent.tsx +++ b/apps/www/app/partners/integrations/IntegrationsContent.tsx @@ -2,13 +2,14 @@ import DefaultLayout from '~/components/Layouts/Default' import SectionContainer from '~/components/Layouts/SectionContainer' +import Panel from '~/components/Panel' import { searchPartners } from '~/lib/marketplaceDb' import type { Partner } from '~/types/partners' -import { ArrowRight, ArrowUpRight, Loader, Search } from 'lucide-react' +import { ArrowRight, ArrowUpRight, LayoutGrid, List, Loader, Search } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { useEffect, useRef, useState } from 'react' -import { Badge, Button, cn, InputGroup, InputGroupAddon, InputGroupInput } from 'ui' +import { Badge, Button, Checkbox, cn, InputGroup, InputGroupAddon, InputGroupInput } from 'ui' import { useDebounce } from 'use-debounce' interface Props { @@ -17,6 +18,8 @@ interface Props { metaDescription: string } +type ViewMode = 'grid' | 'list' + export default function IntegrationsContent({ initialPartners, metaTitle, @@ -25,12 +28,13 @@ export default function IntegrationsContent({ const [partners, setPartners] = useState(initialPartners) const allCategories = Array.from( new Set(initialPartners?.flatMap((p) => p.categories.map((c) => c.name))) - ) + ).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) const [search, setSearch] = useState('') const [debouncedSearchTerm] = useDebounce(search, 300) const [isSearching, setIsSearching] = useState(false) - const [selectedCategory, setSelectedCategory] = useState(null) + const [selectedCategories, setSelectedCategories] = useState([]) + const [viewMode, setViewMode] = useState('grid') const searchIdRef = useRef(0) useEffect(() => { @@ -39,183 +43,307 @@ export default function IntegrationsContent({ setPartners(initialPartners) return } - setIsSearching(true) const currentSearchId = ++searchIdRef.current - searchPartners(debouncedSearchTerm) .then((results) => { - if (currentSearchId === searchIdRef.current) { - setPartners(results ?? []) - } + if (currentSearchId === searchIdRef.current) setPartners(results ?? []) }) .catch(() => { - if (currentSearchId === searchIdRef.current) { - setPartners([]) - } + if (currentSearchId === searchIdRef.current) setPartners([]) }) .finally(() => { - if (currentSearchId === searchIdRef.current) { - setIsSearching(false) - } + if (currentSearchId === searchIdRef.current) setIsSearching(false) }) }, [debouncedSearchTerm, initialPartners]) - const featuredPartners = partners.filter((p) => p.featured).slice(0, 6) - const featuredSlugs = new Set(featuredPartners.map((p) => p.slug)) - const hasSearchQuery = search.trim() !== '' - const isFiltered = hasSearchQuery || selectedCategory !== null - const showFeatured = !isFiltered && featuredPartners.length > 0 - const listPartners = hasSearchQuery - ? partners - : selectedCategory - ? partners.filter((p) => p.categories.some((c) => c.name === selectedCategory)) - : showFeatured - ? partners.filter((p) => !featuredSlugs.has(p.slug)) - : partners + const handleCategoryChange = (category: string) => + setSelectedCategories((prev) => + prev.includes(category) ? prev.filter((c) => c !== category) : [...prev, category] + ) + + const HAS_ACTIVE_FILTERS = search.trim() !== '' || selectedCategories.length > 0 + + const categoryFiltered = + selectedCategories.length > 0 + ? partners.filter((p) => p.categories.some((c) => selectedCategories.includes(c.name))) + : partners + + const featuredPartners = categoryFiltered.filter((p) => p.featured) + const listPartners = HAS_ACTIVE_FILTERS + ? [ + ...categoryFiltered.filter((p) => p.featured), + ...categoryFiltered.filter((p) => !p.featured), + ] + : categoryFiltered.filter((p) => !p.featured) + const showFeatured = !HAS_ACTIVE_FILTERS && featuredPartners.length > 0 + const totalCount = showFeatured + ? featuredPartners.length + listPartners.length + : listPartners.length return ( - - -
+ + +
Partner Catalog -

{metaTitle}

-

{metaDescription}

+

{metaTitle}

+

+ {metaDescription} +

-
- {/* Search + category pills (sticky) */} -
-
+
+ {/* Left sidebar — sticky search + filters */} +
+
+ + + setSearch(e.target.value)} /> - - - {isSearching && ( - - - + )} -
-
- - {allCategories.map((category) => ( - - ))} + Clear all filters +
- {/* Featured grid — shown when not filtering */} - {showFeatured && ( -
- {featuredPartners.map((p) => ( - -
-
- {p.title} -
-
-

- {p.title} -

-
- {p.categories.map((c) => ( - {c.name} - ))} + {/* Right: content */} +
+ {/* Featured partners */} + {showFeatured && ( +
+

Featured

+
+ {featuredPartners.map((p) => ( + +
+
+ {p.title} +
+
+

+ {p.title} +

+
+ {p.categories.map((c) => ( + {c.name} + ))} +
+
-
-
-

- {p.description} -

- - ))} -
- )} +

+ {p.description} +

+ + ))} +
+
+ )} - {/* Compact list */} - {listPartners.length ? ( -
- {listPartners.map((p) => ( - + + {listPartners.length} integration{listPartners.length !== 1 ? 's' : ''} + +
+ + +
- ) : ( -

No partners found.

- )} + + {/* Grid view */} + {viewMode === 'grid' && + (listPartners.length ? ( +
+ {listPartners.map((p) => ( + + +
+ {p.title} +
+
+

+ {p.title} +

+

+ {p.description} +

+
+ {p.categories.map((c) => ( + + {c.name} + + ))} +
+
+
+ + ))} +
+ ) : ( +

+ No integrations found with these filters. +

+ ))} + + {/* List view */} + {viewMode === 'list' && + (listPartners.length ? ( +
+ {listPartners.map((p) => ( + +
+ {p.title} +
+
+
+ + {p.title} + +
+ {p.categories.map((c) => ( + + {c.name} + + ))} +
+
+

{p.description}

+
+ + + ))} +
+ ) : ( +

+ No integrations found with these filters. +

+ ))} +
+