11'use client' ;
22
3- import { useState , useEffect , useRef , useMemo } from 'react' ;
4- import { useSearchParams } from 'next/navigation' ;
5- import { gsap } from '@lib/gsap' ;
6- import { Search , Layers , Database , Sparkles } from 'lucide-react' ;
7- import {
8- getFlattenedApis ,
9- getApiCategories ,
10- filterApis
11- } from '@/lib/api-service' ;
3+ import { useState , useMemo } from 'react' ;
4+ import { motion , AnimatePresence } from 'framer-motion' ;
5+ import { PublicApi } from '@/lib/api-service' ;
126import ApiCard from '@/components/modules/apilab/ApiCard' ;
13- import EmptyState from '@/components/shared/EmptyState' ;
14- import TouchScale from '@/components/shared/TouchScale' ;
7+ import { Search , Sparkles , SlidersHorizontal , ArrowUpDown } from 'lucide-react' ;
158import styles from './page.module.css' ;
169
17- const ITEMS_PER_PAGE = 24 ;
18-
19- export default function ApiLabClient ( ) {
20- const searchParams = useSearchParams ( ) ;
21- const [ activeCategory , setActiveCategory ] = useState ( 'All' ) ;
22- const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
23- const [ visibleCount , setVisibleCount ] = useState ( ITEMS_PER_PAGE ) ;
24- const [ prevId , setPrevId ] = useState < string | null > ( null ) ;
25- const containerRef = useRef < HTMLDivElement > ( null ) ;
26- const loadMoreRef = useRef < HTMLDivElement > ( null ) ;
27-
28- const currentId = searchParams . get ( 'id' ) ;
29- const allApis = useMemo ( ( ) => getFlattenedApis ( ) , [ ] ) ;
30- const categories = useMemo ( ( ) => getApiCategories ( ) , [ ] ) ;
31-
32- // Sync with URL ID
33- if ( currentId !== prevId ) {
34- setPrevId ( currentId ) ;
35- if ( currentId ) {
36- const api = allApis . find ( a => a . id === currentId ) ;
37- if ( api ) {
38- setSearchTerm ( api . name ) ;
39- setActiveCategory ( api . category ) ;
40- }
41- }
42- }
10+ interface ApiLabClientProps {
11+ initialApis : PublicApi [ ] ;
12+ }
4313
14+ /**
15+ * Industrial-grade API Discovery Dashboard.
16+ * Optimized for high-density data and tactile feedback.
17+ */
18+ export default function ApiLabClient ( { initialApis } : ApiLabClientProps ) {
19+ const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
20+ const [ selectedCategory , setSelectedCategory ] = useState ( 'All' ) ;
21+
22+ // Categorization Logic
23+ const categories = useMemo ( ( ) => {
24+ const cats = Array . from ( new Set ( initialApis . map ( api => api . category ) ) ) ;
25+ return [ 'All' , ...cats . sort ( ) ] ;
26+ } , [ initialApis ] ) ;
27+
28+ // Filtering Logic
4429 const filteredApis = useMemo ( ( ) => {
45- return filterApis ( allApis , searchTerm , activeCategory ) ;
46- } , [ allApis , searchTerm , activeCategory ] ) ;
47-
48- const visibleApis = filteredApis . slice ( 0 , visibleCount ) ;
49-
50- // GSAP Entrance
51- useEffect ( ( ) => {
52- const ctx = gsap . context ( ( ) => {
53- gsap . from ( ".api-card-wrapper" , {
54- y : 40 ,
55- opacity : 0 ,
56- stagger : 0.05 ,
57- duration : 0.8 ,
58- ease : "expo.out" ,
59- clearProps : "all"
60- } ) ;
61- } , containerRef ) ;
62- return ( ) => ctx . revert ( ) ;
63- } , [ activeCategory , searchTerm , visibleCount ] ) ;
64-
65- // Infinite Scroll Observer
66- useEffect ( ( ) => {
67- const observer = new IntersectionObserver ( ( entries ) => {
68- if ( entries [ 0 ] . isIntersecting && visibleCount < filteredApis . length ) {
69- setVisibleCount ( prev => prev + ITEMS_PER_PAGE ) ;
70- }
71- } , { threshold : 0.1 } ) ;
72-
73- if ( loadMoreRef . current ) observer . observe ( loadMoreRef . current ) ;
74- return ( ) => observer . disconnect ( ) ;
75- } , [ filteredApis . length , visibleCount ] ) ;
30+ return initialApis . filter ( api => {
31+ const matchesSearch = api . name . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ||
32+ api . description . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ;
33+ const matchesCategory = selectedCategory === 'All' || api . category === selectedCategory ;
34+ return matchesSearch && matchesCategory ;
35+ } ) ;
36+ } , [ initialApis , searchQuery , selectedCategory ] ) ;
7637
7738 return (
78- < div className = { styles . dashboardContainer } ref = { containerRef } style = { { paddingTop : '8rem' } } >
79- { /* Sidebar Navigation */ }
39+ < div className = { styles . dashboardContainer } >
40+ { /* STICKY SIDEBAR */ }
8041 < aside className = { styles . sidebar } >
8142 < div className = { styles . sidebarHeader } >
8243 < div className = "inline-flex items-center gap-2 px-3 py-1 rounded-full bg-accent/10 border border-accent/20 text-accent text-[10px] font-bold uppercase tracking-widest mb-4" >
@@ -87,77 +48,78 @@ export default function ApiLabClient() {
8748 < p className = { styles . miniSubtitle } > Discovery & Integration </ p >
8849 </ div >
8950
90- < div className = { styles . searchBox } >
91- < TouchScale scale = { 0.98 } >
92- < div className = "relative group" >
93- < Search className = "absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500 group-focus-within:text-accent transition-colors" />
51+ < div className = { styles . sidebarSection } >
52+ < label className = { styles . sidebarLabel } > Global Discovery </ label >
53+ < div className = { styles . searchWrapper } >
54+ < Search className = { styles . searchIcon } />
9455 < input
95- type = "text"
96- placeholder = "Search streams ..."
97- className = { styles . sidebarSearch }
98- value = { searchTerm }
99- onChange = { ( e ) => setSearchTerm ( e . target . value ) }
56+ type = "text"
57+ placeholder = "Search Lab ..."
58+ className = { styles . searchInput }
59+ value = { searchQuery }
60+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
10061 />
101- </ div >
102- </ TouchScale >
62+ </ div >
10363 </ div >
10464
105- < div className = "mt-4" >
106- < h3 className = { styles . sidebarTitle } > Categories</ h3 >
107- < nav className = { styles . categoryList } >
108- < TouchScale scale = { 0.98 } className = "w-full" >
109- < button
110- onClick = { ( ) => setActiveCategory ( 'All' ) }
111- className = { `${ styles . filterBtn } ${ activeCategory === 'All' ? styles . active : '' } w-full flex items-center gap-2` }
112- >
113- < Layers className = "w-4 h-4" />
114- All Streams
115- </ button >
116- </ TouchScale >
117- { categories . map ( ( cat ) => (
118- < TouchScale key = { cat . name } scale = { 0.98 } className = "w-full" >
119- < button
120- onClick = { ( ) => setActiveCategory ( cat . name ) }
121- className = { `${ styles . filterBtn } ${ activeCategory === cat . name ? styles . active : '' } w-full flex items-center justify-between` }
122- >
123- < span > { cat . name } </ span >
124- < span className = "text-[10px] opacity-50" > { cat . count } </ span >
125- </ button >
126- </ TouchScale >
127- ) ) }
128- </ nav >
65+ < div className = { styles . sidebarSection } >
66+ < div className = "flex items-center justify-between mb-4" >
67+ < label className = { styles . sidebarLabel } > Categories</ label >
68+ < SlidersHorizontal className = "w-3 h-3 text-gray-500" />
69+ </ div >
70+ < div className = { styles . categoryList } >
71+ { categories . map ( cat => (
72+ < button
73+ key = { cat }
74+ onClick = { ( ) => setSelectedCategory ( cat ) }
75+ className = { `${ styles . categoryBtn } ${ selectedCategory === cat ? styles . active : '' } ` }
76+ >
77+ { cat }
78+ { selectedCategory === cat && < motion . div layoutId = "sidebar-active" className = { styles . activeIndicator } /> }
79+ </ button >
80+ ) ) }
81+ </ div >
12982 </ div >
13083 </ aside >
13184
132- { /* Main Results Area */ }
133- < main className = { styles . mainContent } >
134- < div className = { styles . resultMeta } >
85+ { /* MAIN DISCOVERY GRID */ }
86+ < main className = { styles . discoveryContent } >
87+ < div className = { styles . discoveryHeader } >
13588 < div className = "flex items-center gap-4" >
136- < Database className = "w-4 h-4" />
137- < span > Showing { filteredApis . length } artifacts across { activeCategory } </ span >
89+ < div className = { styles . statBox } >
90+ < span className = { styles . statValue } > { filteredApis . length } </ span >
91+ < span className = { styles . statLabel } > Resources Found</ span >
92+ </ div >
13893 </ div >
94+ < button className = { styles . sortBtn } >
95+ < ArrowUpDown className = "w-4 h-4" />
96+ Latest Release
97+ </ button >
13998 </ div >
14099
141- { visibleApis . length > 0 ? (
142- < div className = { styles . grid } >
143- { visibleApis . map ( ( api ) => (
144- < ApiCard key = { api . id } api = { api } />
100+ { /* Liquid Grid with Staggered Reveals */ }
101+ < div className = { styles . gridContainer } >
102+ < AnimatePresence mode = "popLayout" >
103+ { filteredApis . slice ( 0 , 40 ) . map ( ( api , index ) => (
104+ < motion . div
105+ key = { api . name }
106+ layout
107+ initial = { { opacity : 0 , y : 30 , scale : 0.95 } }
108+ animate = { { opacity : 1 , y : 0 , scale : 1 } }
109+ exit = { { opacity : 0 , scale : 0.95 , transition : { duration : 0.2 } } }
110+ transition = { {
111+ type : "spring" ,
112+ stiffness : 100 ,
113+ damping : 20 ,
114+ delay : index * 0.05 ,
115+ restDelta : 0.001
116+ } }
117+ >
118+ < ApiCard api = { api } />
119+ </ motion . div >
145120 ) ) }
146- </ div >
147- ) : (
148- < EmptyState
149- title = "Data Stream Empty"
150- message = { `Zero results detected for "${ searchTerm } " in the current matrix.` }
151- icon = "filter"
152- />
153- ) }
154-
155- { /* Load More Indicator */ }
156- { visibleCount < filteredApis . length && (
157- < div ref = { loadMoreRef } className = { styles . loader } >
158- < div className = { styles . spinner } />
159- </ div >
160- ) }
121+ </ AnimatePresence >
122+ </ div >
161123 </ main >
162124 </ div >
163125 ) ;
0 commit comments