Skip to content

Commit fa06949

Browse files
committed
feat: Implement module search and filtering functionality on the hub page.
1 parent 7150072 commit fa06949

File tree

9 files changed

+644
-82
lines changed

9 files changed

+644
-82
lines changed

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
<script type="module" src="./src/main.tsx"></script>
1414
</body>
1515

16-
</html>
16+
</html>

src/App.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Production-grade React + TypeScript application
44
*/
55

6-
import { NavigationProvider, ConsoleProvider, useNavigation, isModuleView } from './contexts';
6+
import { NavigationProvider, ConsoleProvider, SearchProvider, useNavigation, isModuleView } from './contexts';
77
import { Layout, ModuleHub, ModuleTabs } from './components';
88
import { GCModule, ThreadsModule, JITModule, JMMModule, StaticModule, ArchitectureModule, PlatformModule } from './features';
99
import type { ModuleId } from './types';
@@ -57,7 +57,9 @@ function App() {
5757
return (
5858
<NavigationProvider>
5959
<ConsoleProvider>
60-
<AppContent />
60+
<SearchProvider>
61+
<AppContent />
62+
</SearchProvider>
6163
</ConsoleProvider>
6264
</NavigationProvider>
6365
);

src/components/Hub/ModuleHub.module.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,38 @@
9595
background: var(--color-bg-secondary);
9696
padding: var(--space-1) var(--space-2);
9797
border-radius: var(--radius-sm);
98+
}
99+
100+
/* No Results State */
101+
.noResults {
102+
display: flex;
103+
flex-direction: column;
104+
align-items: center;
105+
justify-content: center;
106+
padding: var(--space-16) var(--space-4);
107+
text-align: center;
108+
}
109+
110+
.noResultsIcon {
111+
font-size: 48px;
112+
margin-bottom: var(--space-4);
113+
}
114+
115+
.noResults p {
116+
color: var(--color-text-secondary);
117+
margin: 0;
118+
}
119+
120+
.noResultsHint {
121+
font-size: var(--text-sm);
122+
color: var(--color-text-muted) !important;
123+
margin-top: var(--space-2) !important;
124+
}
125+
126+
/* Module Count */
127+
.moduleCount {
128+
text-align: center;
129+
color: var(--color-text-muted);
130+
font-size: var(--text-sm);
131+
margin-top: var(--space-8);
98132
}

src/components/Hub/ModuleHub.tsx

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
11
/**
22
* ModuleHub Component
3-
* Topic selection grid for the main hub page
3+
* Topic selection grid with search and filtering
44
*/
55

6-
import { useNavigation } from '../../contexts';
6+
import { useMemo } from 'react';
7+
import { useNavigation, useSearch } from '../../contexts';
78
import { MODULE_REGISTRY } from '../../types';
89
import styles from './ModuleHub.module.css';
910

1011
export function ModuleHub() {
1112
const { navigateTo } = useNavigation();
12-
const modules = Object.values(MODULE_REGISTRY);
13+
const { searchQuery, filterBadge } = useSearch();
14+
const allModules = Object.values(MODULE_REGISTRY);
15+
16+
// Filter modules based on search and badge
17+
const filteredModules = useMemo(() => {
18+
return allModules.filter((module) => {
19+
// Search filter
20+
const matchesSearch = searchQuery === '' ||
21+
module.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
22+
module.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
23+
module.subtitle.toLowerCase().includes(searchQuery.toLowerCase());
24+
25+
// Badge filter
26+
const matchesBadge = filterBadge === 'all' || module.badge === filterBadge;
27+
28+
return matchesSearch && matchesBadge;
29+
});
30+
}, [allModules, searchQuery, filterBadge]);
1331

1432
return (
1533
<div className={styles.hubContainer}>
@@ -21,35 +39,48 @@ export function ModuleHub() {
2139
</p>
2240
</div>
2341

24-
<div className={styles.grid}>
25-
{modules.map((module) => (
26-
<div
27-
key={module.id}
28-
className={styles.card}
29-
onClick={() => navigateTo(module.id)}
30-
>
31-
<div className={styles.cardHeader}>
42+
{filteredModules.length === 0 ? (
43+
<div className={styles.noResults}>
44+
<span className={styles.noResultsIcon}>🔍</span>
45+
<p>No modules found matching your search.</p>
46+
<p className={styles.noResultsHint}>Try a different search term or filter.</p>
47+
</div>
48+
) : (
49+
<div className={styles.grid}>
50+
{filteredModules.map((module) => (
51+
<div
52+
key={module.id}
53+
className={styles.card}
54+
onClick={() => navigateTo(module.id)}
55+
>
56+
<div className={styles.cardHeader}>
57+
<span
58+
className={styles.indicator}
59+
style={{
60+
backgroundColor: module.accentColor,
61+
boxShadow: `0 0 10px ${module.glowColor}`,
62+
}}
63+
/>
64+
<h3 className={styles.cardTitle}>{module.title}</h3>
65+
</div>
66+
<p className={styles.cardDescription}>{module.description}</p>
3267
<span
33-
className={styles.indicator}
68+
className={styles.badge}
3469
style={{
35-
backgroundColor: module.accentColor,
36-
boxShadow: `0 0 10px ${module.glowColor}`,
70+
color: module.accentColor,
3771
}}
38-
/>
39-
<h3 className={styles.cardTitle}>{module.title}</h3>
72+
>
73+
{module.badge}
74+
</span>
4075
</div>
41-
<p className={styles.cardDescription}>{module.description}</p>
42-
<span
43-
className={styles.badge}
44-
style={{
45-
color: module.accentColor,
46-
}}
47-
>
48-
{module.badge}
49-
</span>
50-
</div>
51-
))}
52-
</div>
76+
))}
77+
</div>
78+
)}
79+
80+
{/* Module Count */}
81+
<p className={styles.moduleCount}>
82+
Showing {filteredModules.length} of {allModules.length} modules
83+
</p>
5384
</div>
5485
</div>
5586
);

src/components/Layout/Header.module.css

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
align-items: center;
88
box-shadow: var(--shadow-lg);
99
z-index: 20;
10+
gap: var(--space-4);
11+
flex-wrap: wrap;
1012
}
1113

1214
.leftSection {
@@ -54,7 +56,7 @@
5456
.logo {
5557
width: 40px;
5658
height: 40px;
57-
background: linear-gradient(135deg, #2563eb, #4f46e5);
59+
background: linear-gradient(135deg, #f97316, #ea580c);
5860
border-radius: var(--radius-lg);
5961
display: flex;
6062
align-items: center;
@@ -87,4 +89,106 @@
8789
.rightSection {
8890
display: flex;
8991
gap: var(--space-4);
92+
}
93+
94+
/* Search Section */
95+
.searchSection {
96+
display: flex;
97+
align-items: center;
98+
gap: var(--space-3);
99+
flex: 1;
100+
max-width: 500px;
101+
margin-left: auto;
102+
}
103+
104+
.searchBox {
105+
flex: 1;
106+
position: relative;
107+
display: flex;
108+
align-items: center;
109+
}
110+
111+
.searchIcon {
112+
position: absolute;
113+
left: var(--space-3);
114+
width: 18px;
115+
height: 18px;
116+
color: var(--color-text-muted);
117+
pointer-events: none;
118+
}
119+
120+
.searchInput {
121+
width: 100%;
122+
padding: var(--space-2) var(--space-3);
123+
padding-left: 40px;
124+
padding-right: 32px;
125+
background-color: var(--color-bg-secondary);
126+
border: 1px solid var(--color-border-default);
127+
border-radius: var(--radius-lg);
128+
color: var(--color-text-primary);
129+
font-size: var(--text-sm);
130+
transition: all var(--transition-fast);
131+
}
132+
133+
.searchInput::placeholder {
134+
color: var(--color-text-muted);
135+
}
136+
137+
.searchInput:focus {
138+
outline: none;
139+
border-color: #f97316;
140+
box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2);
141+
}
142+
143+
.clearButton {
144+
position: absolute;
145+
right: var(--space-2);
146+
width: 24px;
147+
height: 24px;
148+
display: flex;
149+
align-items: center;
150+
justify-content: center;
151+
background: transparent;
152+
border: none;
153+
color: var(--color-text-muted);
154+
cursor: pointer;
155+
font-size: var(--text-lg);
156+
border-radius: var(--radius-full);
157+
transition: all var(--transition-fast);
158+
}
159+
160+
.clearButton:hover {
161+
color: var(--color-text-primary);
162+
background-color: rgba(255, 255, 255, 0.1);
163+
}
164+
165+
.filterSelect {
166+
padding: var(--space-2) var(--space-3);
167+
background-color: var(--color-bg-secondary);
168+
border: 1px solid var(--color-border-default);
169+
border-radius: var(--radius-lg);
170+
color: var(--color-text-primary);
171+
font-size: var(--text-sm);
172+
cursor: pointer;
173+
min-width: 140px;
174+
transition: all var(--transition-fast);
175+
}
176+
177+
.filterSelect:focus {
178+
outline: none;
179+
border-color: #f97316;
180+
}
181+
182+
.filterSelect option {
183+
background-color: #0f172a;
184+
color: var(--color-text-primary);
185+
}
186+
187+
@media (max-width: 768px) {
188+
.searchSection {
189+
order: 3;
190+
flex-basis: 100%;
191+
max-width: none;
192+
margin-left: 0;
193+
}
90194
}

src/components/Layout/Header.tsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
/**
22
* Header Component
3-
* Main application header with navigation
3+
* Main application header with navigation and search
44
*/
55

66
import type { ReactNode } from 'react';
7-
import { useNavigation, isModuleView } from '../../contexts';
7+
import { useNavigation, isModuleView, useSearch, type FilterBadge } from '../../contexts';
88
import { MODULE_REGISTRY } from '../../types';
99
import styles from './Header.module.css';
1010

11+
const FILTER_OPTIONS: { value: FilterBadge; label: string }[] = [
12+
{ value: 'all', label: 'All Modules' },
13+
{ value: 'Interactive', label: 'Interactive' },
14+
{ value: 'Advanced', label: 'Advanced' },
15+
{ value: 'Foundation', label: 'Foundation' },
16+
{ value: 'Monitor Sim', label: 'Monitor Sim' },
17+
{ value: 'Profiler Sim', label: 'Profiler Sim' },
18+
{ value: 'Cache Lab', label: 'Cache Lab' },
19+
{ value: 'Memory Lab', label: 'Memory Lab' },
20+
];
21+
1122
interface HeaderProps {
1223
moduleNav?: ReactNode;
1324
}
1425

1526
export function Header({ moduleNav }: HeaderProps) {
1627
const { currentView, goToHub } = useNavigation();
28+
const { searchQuery, filterBadge, setSearchQuery, setFilterBadge } = useSearch();
1729
const isInModule = isModuleView(currentView);
1830
const moduleConfig = isInModule ? MODULE_REGISTRY[currentView] : null;
1931

@@ -54,6 +66,38 @@ export function Header({ moduleNav }: HeaderProps) {
5466
</div>
5567
</div>
5668

69+
{/* Search & Filter (only on hub) */}
70+
{!isInModule && (
71+
<div className={styles.searchSection}>
72+
<div className={styles.searchBox}>
73+
<svg className={styles.searchIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
74+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
75+
</svg>
76+
<input
77+
type="text"
78+
className={styles.searchInput}
79+
placeholder="Search modules..."
80+
value={searchQuery}
81+
onChange={(e) => setSearchQuery(e.target.value)}
82+
/>
83+
{searchQuery && (
84+
<button className={styles.clearButton} onClick={() => setSearchQuery('')}>
85+
×
86+
</button>
87+
)}
88+
</div>
89+
<select
90+
className={styles.filterSelect}
91+
value={filterBadge}
92+
onChange={(e) => setFilterBadge(e.target.value as FilterBadge)}
93+
>
94+
{FILTER_OPTIONS.map(opt => (
95+
<option key={opt.value} value={opt.value}>{opt.label}</option>
96+
))}
97+
</select>
98+
</div>
99+
)}
100+
57101
{/* Module Navigation */}
58102
{isInModule && moduleNav && (
59103
<div className={styles.rightSection}>{moduleNav}</div>

0 commit comments

Comments
 (0)