diff --git a/src/components/EcosystemContracts/TabsView.tsx b/src/components/EcosystemContracts/TabsView.tsx new file mode 100644 index 00000000..d8f7b63e --- /dev/null +++ b/src/components/EcosystemContracts/TabsView.tsx @@ -0,0 +1,193 @@ +'use client'; + +import * as React from 'react'; + +type GroupedContracts = { + projectName: string; + contracts: Record[]; + contractCount: number; +}; + +interface EcosystemContractsTabsProps { + groupedData: GroupedContracts[]; + nameKey: string; + addressKey: string; +} + +export function EcosystemContractsTabs({ groupedData, nameKey, addressKey }: EcosystemContractsTabsProps) { + const [query, setQuery] = React.useState(''); + const [openSections, setOpenSections] = React.useState>({}); + + const slugify = (value: string): string => { + return String(value) + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + }; + + const toggleSection = (projectName: string) => { + setOpenSections((prev) => ({ ...prev, [projectName]: !prev[projectName] })); + }; + + const handleSearchSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const q = query.trim().toLowerCase(); + if (!q) return; + + for (const group of groupedData) { + for (const contract of group.contracts) { + const name = String(contract[nameKey] || '').toLowerCase(); + const address = String(contract[addressKey] || '').toLowerCase(); + if (name.includes(q) || address.includes(q)) { + setOpenSections((prev) => ({ ...prev, [group.projectName]: true })); + const id = `contract-${slugify(group.projectName)}-${address}`; + requestAnimationFrame(() => { + const el = document.getElementById(id); + if (!el) return; + + // Center the element in the viewport for better visibility (works for both and card
) + const rect = el.getBoundingClientRect(); + const currentScrollY = window.scrollY || window.pageYOffset; + const absoluteTop = rect.top + currentScrollY; + const viewportHeight = window.innerHeight; + const targetTop = absoluteTop - Math.max(0, (viewportHeight - rect.height) / 2); + + const maxScroll = Math.max(0, (document.documentElement.scrollHeight || document.body.scrollHeight) - viewportHeight); + const clampedTop = Math.min(Math.max(0, targetTop), maxScroll); + + window.scrollTo({ top: clampedTop, behavior: 'smooth' }); + }); + return; + } + } + } + }; + + return ( +
+
+ setQuery(e.target.value)} + placeholder='Search by contract name or address' + aria-label='Search by contract name or address' + autoComplete='off' + className='w-full px-3 py-2 rounded border border-neutral-300 dark:border-neutral-700 bg-neutral-50 dark:!bg-neutral-900 text-sm text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-red-500/25 dark:focus:ring-red-500/20 focus:border-red-500 dark:focus:border-red-500 transition-colors' + /> + +
+ +
+ {groupedData.map((group) => { + const isOpen = !!openSections[group.projectName]; + const sectionId = `section-${slugify(group.projectName)}`; + return ( +
+ +
+
+ {/* Desktop table view */} +
+ + + + + + + + + {group.contracts.map((contract, contractIndex) => { + const addr = String(contract[addressKey] || '').toLowerCase(); + const id = `contract-${slugify(group.projectName)}-${addr}`; + return ( + + + + + ); + })} + +
+ Contract Name + + Contract Address +
+ {contract[nameKey] || 'Unnamed Contract'} + +
+ {contract[addressKey]} + + SeiScan ↗ + +
+
+
+ + {/* Mobile card view */} +
+ {group.contracts.map((contract, contractIndex) => { + const addr = String(contract[addressKey] || '').toLowerCase(); + const id = `contract-${slugify(group.projectName)}-${addr}`; + return ( +
+
+
+ Contract Name + {contract[nameKey] || 'Unnamed Contract'} +
+
+ Contract Address +
+ {contract[addressKey]} + + SeiScan ↗ + +
+
+
+
+ ); + })} +
+
+
+
+ ); + })} +
+
+ ); +} + +export default EcosystemContractsTabs; diff --git a/src/components/EcosystemContracts/index.tsx b/src/components/EcosystemContracts/index.tsx index fcde3018..02f339c7 100644 --- a/src/components/EcosystemContracts/index.tsx +++ b/src/components/EcosystemContracts/index.tsx @@ -1,4 +1,5 @@ import { Callout } from 'nextra/components'; +import EcosystemContractsTabs from './TabsView'; // CSV file component - reads a local CSV file at build time // Just download your Google Sheet as CSV and place it in your project @@ -81,35 +82,6 @@ export async function RemoteSheetData() { // ---------------------------- helpers ---------------------------- - const ContractLink = ({ address, name }: { address: string; name: string }) => { - if (!address || !address.trim()) { - return No contract address; - } - - // Clean the address - const cleanAddress = address.trim(); - - let explorerName = 'SeiScan'; - - let explorerUrl = `https://seiscan.io/address/${cleanAddress}`; - const shortAddress = `${cleanAddress}`; - - return ( -
- {shortAddress} - {explorerUrl !== '#' && ( - - {explorerName} ↗ - - )} -
- ); - }; - const groupDataByProject = (data: any[]) => { if (!data || data.length === 0) return []; @@ -141,81 +113,7 @@ export async function RemoteSheetData() { const groupedData = groupDataByProject(data); const headers = Object.keys(data[0]); - const totalContracts = data.length; - const totalProjects = groupedData.length; - - return ( -
- {/* Summary stats */} - - {/* Project groups */} - {groupedData.map((group, groupIndex) => ( -
- {/* Project header */} -
-
-

{group.projectName}

- - {group.contractCount} contract{group.contractCount !== 1 ? 's' : ''} - -
-
#{groupIndex + 1}
-
- - {/* Contracts table for this project */} -
- - - - - - - - - - {group.contracts.map((contract, contractIndex) => ( - - - - - - ))} - -
# - Contract Name - - Contract Address -
{contractIndex + 1} - {contract[headers[2]] || 'Unnamed Contract'} - - -
-
- - {/* Mobile card view for this project */} -
- {group.contracts.map((contract, contractIndex) => ( -
-
- Contract #{contractIndex + 1} -
-
-
- Contract Name - {contract[headers[2]] || 'Unnamed Contract'} -
-
- Contract Address - -
-
-
- ))} -
-
- ))} -
- ); + return ; }; // ---------------------------- rendering ---------------------------- diff --git a/src/components/Tabs/index.tsx b/src/components/Tabs/index.tsx index 6ec254d1..a37db4a0 100644 --- a/src/components/Tabs/index.tsx +++ b/src/components/Tabs/index.tsx @@ -5,6 +5,8 @@ import * as TabsPrimitive from '@radix-ui/react-tabs'; interface TabsProps { defaultValue: string; + value?: string; + onValueChange?: (value: string) => void; className?: string; children: React.ReactNode; } @@ -26,8 +28,8 @@ interface TabsContentProps { children: React.ReactNode; } -export const Tabs = React.forwardRef(({ defaultValue, className, children }, ref) => ( - +export const Tabs = React.forwardRef(({ defaultValue, value, onValueChange, className, children }, ref) => ( + {children} ));