diff --git a/apps/web/src/components/Ecosystem/Card.tsx b/apps/web/src/components/Ecosystem/Card.tsx index 6309ced00b3..2fbf3cf3be2 100644 --- a/apps/web/src/components/Ecosystem/Card.tsx +++ b/apps/web/src/components/Ecosystem/Card.tsx @@ -1,26 +1,48 @@ 'use client'; + +/** + * Ecosystem Card Component + * + * Displays an individual ecosystem project/app in a card format. + * Includes project logo, name, description, and category tags. + */ + import ImageWithLoading from 'apps/web/src/components/ImageWithLoading'; import Card from 'apps/web/src/components/base-org/Card'; import Text from 'apps/web/src/components/base-org/typography/TextRedesign'; import { TextVariant } from 'apps/web/src/components/base-org/typography/TextRedesign/types'; import Title from 'apps/web/src/components/base-org/typography/TitleRedesign'; import { TitleLevel } from 'apps/web/src/components/base-org/typography/TitleRedesign/types'; +import type { EcosystemApp } from 'apps/web/src/types/ecosystem'; -type Props = { - name: string; - url: string; - description: string; - imageUrl: string; - category: string; - subcategory: string; -}; +type Props = EcosystemApp; const MAX_DESCRIPTION_LENGTH = 200; -function getNiceDomainDisplayFromUrl(url: string) { +/** + * Extracts a clean domain name from a URL for display purposes. + * Removes protocol and www prefix, and strips path segments. + * + * @param url - The full URL to extract the domain from + * @returns The cleaned domain name (e.g., "example.com") + * + * @example + * getNiceDomainDisplayFromUrl("https://www.example.com/path") // returns "example.com" + */ +function getNiceDomainDisplayFromUrl(url: string): string { return url.replace('https://', '').replace('http://', '').replace('www.', '').split('/')[0]; } +/** + * EcosystemCard displays a single ecosystem project in a clickable card format. + * + * Features: + * - Project logo with loading state + * - Project name and domain + * - Truncated description (max 200 characters) + * - Category and subcategory tags + * - Opens project URL in a new tab + */ export default function EcosystemCard({ name, url, @@ -39,7 +61,8 @@ export default function EcosystemCard({ href={url} rel="noreferrer noopener" target="_blank" - className="flex flex-col items-stretch w-full h-full justify-stretch" + aria-label={`Visit ${name} - ${category} project. ${truncatedDescription}`} + className="flex flex-col items-stretch w-full h-full justify-stretch focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 rounded-lg" > = { ai: ['ai'], wallet: ['account abstraction', 'multisig', 'self-custody'], diff --git a/apps/web/src/components/Ecosystem/List.tsx b/apps/web/src/components/Ecosystem/List.tsx index 824f369d2bb..1cb55e293b0 100644 --- a/apps/web/src/components/Ecosystem/List.tsx +++ b/apps/web/src/components/Ecosystem/List.tsx @@ -1,8 +1,16 @@ 'use client'; + +/** + * Ecosystem List Component + * + * Renders a grid of ecosystem app cards with animation support. + * Handles empty states and "load more" pagination. + */ + import classNames from 'classnames'; import { AnimatePresence, motion, cubicBezier } from 'motion/react'; import EcosystemCard from './Card'; -import { EcosystemApp } from 'apps/web/src/components/Ecosystem/Content'; +import type { DecoratedEcosystemApp } from 'apps/web/src/types/ecosystem'; import { Dispatch, SetStateAction, useCallback } from 'react'; import { Button, @@ -23,7 +31,10 @@ const cardAnimations = { exit: { opacity: 0 }, }; -function AnimatedEcosystemCard({ app }: { app: EcosystemApp }) { +/** + * Animated wrapper for ecosystem cards with enter/exit transitions. + */ +function AnimatedEcosystemCard({ app }: { app: DecoratedEcosystemApp }) { return ( >; + /** Callback to clear the search query */ + onClearSearch: () => void; +} + +/** + * List component displays a paginated grid of ecosystem app cards. + * + * Features: + * - Animated card grid with staggered transitions + * - Empty state with helpful messaging + * - "View more" pagination button + * - Responsive grid layout (1-3 columns) + */ export function List({ selectedCategories, searchText, @@ -45,14 +83,7 @@ export function List({ showCount, setShowCount, onClearSearch, -}: { - selectedCategories: string[]; - searchText: string; - apps: EcosystemApp[]; - showCount: number; - setShowCount: Dispatch>; - onClearSearch: () => void; -}) { +}: ListProps) { const canShowMore = showCount < apps.length; const showEmptyState = apps.length === 0; const truncatedApps = apps.slice(0, showCount); diff --git a/apps/web/src/components/Ecosystem/SearchBar.tsx b/apps/web/src/components/Ecosystem/SearchBar.tsx index 917dfa95077..18f3de4ed76 100644 --- a/apps/web/src/components/Ecosystem/SearchBar.tsx +++ b/apps/web/src/components/Ecosystem/SearchBar.tsx @@ -1,4 +1,12 @@ 'use client'; + +/** + * Ecosystem SearchBar Component + * + * An animated, accessible search input for filtering ecosystem apps. + * Features expandable mobile design and keyboard accessibility. + */ + import { Dispatch, SetStateAction, useCallback, useRef, useState, useEffect } from 'react'; import { motion, AnimatePresence, cubicBezier, Variants } from 'motion/react'; @@ -22,6 +30,9 @@ const buttonVariants: Variants = { exit: { opacity: 0 }, }; +/** + * Search icon SVG component + */ function SearchIcon() { return ( @@ -33,6 +44,9 @@ function SearchIcon() { ); } +/** + * Close/clear icon SVG component + */ function XIcon() { return ( @@ -44,13 +58,33 @@ function XIcon() { ); } -export function SearchBar({ - search, - setSearch, -}: { +/** + * Props for the SearchBar component + */ +interface SearchBarProps { + /** Current search query value */ search: string; + /** Callback to update the search query */ setSearch: Dispatch>; -}) { +} + +/** + * SearchBar provides an animated, accessible search input for the ecosystem page. + * + * Features: + * - Expandable design on mobile devices + * - Animated transitions for expand/collapse + * - Clear button when search has content + * - Keyboard accessible with proper focus management + * - ARIA labels for screen reader support + * + * @example + * ```tsx + * const [search, setSearch] = useState(''); + * + * ``` + */ +export function SearchBar({ search, setSearch }: SearchBarProps) { const [isExpanded, setIsExpanded] = useState(false); const debounced = useRef(); const inputRef = useRef(null); @@ -138,8 +172,8 @@ export function SearchBar({ ; + +/** + * Props for ecosystem filter components + */ +export interface EcosystemFilterProps { + /** Currently selected categories */ + selectedCategories: string[]; + /** Currently selected subcategories */ + selectedSubcategories: string[]; + /** Callback to update selected subcategories */ + setSelectedSubcategories: (subcategories: string[]) => void; + /** Category to subcategory configuration */ + config: Record; +} diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts new file mode 100644 index 00000000000..41f3cee38fc --- /dev/null +++ b/apps/web/src/types/index.ts @@ -0,0 +1,23 @@ +/** + * Type exports for the Base web application + * + * This barrel file re-exports all types from the types directory + * for convenient importing throughout the application. + */ + +export type { + EcosystemApp, + EcosystemCategory, + EcosystemSubcategory, + DecoratedEcosystemApp, + EcosystemCategoryConfig, + EcosystemFilterProps, + AISubcategory, + ConsumerSubcategory, + DeFiSubcategory, + InfraSubcategory, + OnrampSubcategory, + WalletSubcategory, +} from './ecosystem'; + +export type { ManagedAddressesData, ManagedAddressesResponse } from './ManagedAddresses';