@@ -6,11 +6,47 @@ interface Props {
66}
77
88const { samples } = Astro .props ;
9+
10+ // Build metadata filters at build time
11+ // Exclude SAMPLE ID (unique per sample, not useful for filtering)
12+ const EXCLUDED_KEYS = new Set ([' SAMPLE ID' ]);
13+ const metadataFilters = new Map <string , Map <string , string >>();
14+
15+ for (const sample of samples ) {
16+ for (const { key, value } of sample .data .metadata ) {
17+ if (EXCLUDED_KEYS .has (key )) continue ;
18+ if (! metadataFilters .has (key )) {
19+ metadataFilters .set (key , new Map ());
20+ }
21+ // Use case-insensitive deduplication, keeping the first encountered form
22+ const valuesMap = metadataFilters .get (key )! ;
23+ const lowerValue = value .toLowerCase ();
24+ if (! valuesMap .has (lowerValue )) {
25+ valuesMap .set (lowerValue , value );
26+ }
27+ }
28+ }
29+
30+ // Sort keys alphabetically, values alphabetically within each key
31+ const sortedFilters = [... metadataFilters .entries ()]
32+ .sort (([a ], [b ]) => a .localeCompare (b ))
33+ .map (([key , valuesMap ]) => [key , [... valuesMap .values ()].sort ((a , b ) => a .localeCompare (b , undefined , { sensitivity: ' base' }))] as const );
34+
35+ // Build per-card metadata JSON for client-side filtering
36+ function getMetadataMap(sample : CollectionEntry <' samples' >): Record <string , string > {
37+ const map: Record <string , string > = {};
38+ for (const { key, value } of sample .data .metadata ) {
39+ if (! EXCLUDED_KEYS .has (key )) {
40+ map [key ] = value ;
41+ }
42+ }
43+ return map ;
44+ }
945---
1046
1147<div id =" sample-gallery" >
1248 <!-- Search bar -->
13- <div class =" mb-10 max-w-xl mx-auto" >
49+ <div class =" mb-6 max-w-xl mx-auto" >
1450 <div class =" relative" >
1551 <svg class =" absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 pointer-events-none" style =" color: var(--text-faint);" fill =" none" viewBox =" 0 0 24 24" stroke =" currentColor" stroke-width =" 2" aria-hidden =" true" >
1652 <path stroke-linecap =" round" stroke-linejoin =" round" d =" M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -24,6 +60,33 @@ const { samples } = Astro.props;
2460 </div >
2561 </div >
2662
63+ { sortedFilters .length > 0 && (
64+ <div id = " metadata-filters" class = " mb-8 flex flex-wrap items-center justify-center gap-4" >
65+ { sortedFilters .map (([key , values ]) => (
66+ <div class = " flex items-center gap-2" >
67+ <label
68+ for = { ` filter-${key .toLowerCase ().replace (/ \s + / g , ' -' )} ` }
69+ class = " text-xs font-medium uppercase tracking-wide"
70+ style = " color: var(--text-faint);"
71+ >
72+ { key }
73+ </label >
74+ <select
75+ id = { ` filter-${key .toLowerCase ().replace (/ \s + / g , ' -' )} ` }
76+ class = " metadata-filter rounded-lg border px-3 py-1.5 text-sm focus:outline-none focus:border-purple-500 transition-all duration-300 cursor-pointer"
77+ style = " background: var(--bg-secondary); border-color: var(--border-secondary); color: var(--text-primary);"
78+ data-filter-key = { key }
79+ >
80+ <option value = " " >All</option >
81+ { values .map ((value ) => (
82+ <option value = { value } >{ value } </option >
83+ ))}
84+ </select >
85+ </div >
86+ ))}
87+ </div >
88+ )}
89+
2790 <p id =" sample-count" class =" text-sm mb-8 text-center" style =" color: var(--text-faint);" >
2891 { samples .length } samples
2992 </p >
@@ -37,6 +100,7 @@ const { samples } = Astro.props;
37100 data-title = { sample .data .title .toLowerCase ()}
38101 data-description = { sample .data .shortDescription .toLowerCase ()}
39102 data-authors = { sample .data .authors .map (a => a .name .toLowerCase ()).join (' ' )}
103+ data-metadata = { JSON .stringify (getMetadataMap (sample ))}
40104 >
41105 { sample .data .thumbnails ?.[0 ] && (
42106 <div class = " overflow-hidden" >
@@ -75,30 +139,59 @@ const { samples } = Astro.props;
75139 </div >
76140
77141 <p id =" no-results" class =" text-center py-16 hidden" style =" color: var(--text-faint);" >
78- No samples match your search.
142+ No samples match your search or filters .
79143 </p >
80144</div >
81145
82146<script >
83147 const searchInput = document.getElementById('sample-search') as HTMLInputElement;
148+ const filterSelects = document.querySelectorAll('.metadata-filter') as NodeListOf<HTMLSelectElement>;
84149 const cards = document.querySelectorAll('.sample-card') as NodeListOf<HTMLElement>;
85150 const countEl = document.getElementById('sample-count')!;
86151 const noResults = document.getElementById('no-results')!;
87152
88- searchInput.addEventListener('input', () => {
153+ function applyFilters() {
89154 const query = searchInput.value.toLowerCase().trim();
155+
156+ // Collect active metadata filters
157+ const activeFilters: Record<string, string> = {};
158+ filterSelects.forEach((select) => {
159+ const key = select.dataset.filterKey!;
160+ const value = select.value;
161+ if (value) {
162+ activeFilters[key] = value;
163+ }
164+ });
165+
90166 let visible = 0;
91167
92168 cards.forEach((card) => {
93169 const title = card.dataset.title || '';
94170 const description = card.dataset.description || '';
95171 const authors = card.dataset.authors || '';
96- const matches = !query || title.includes(query) || description.includes(query) || authors.includes(query);
172+ const metadata: Record<string, string> = JSON.parse(card.dataset.metadata || '{}');
173+
174+ // Text search match
175+ const textMatch = !query || title.includes(query) || description.includes(query) || authors.includes(query);
176+
177+ // Metadata filter match (all active filters must match)
178+ let metadataMatch = true;
179+ for (const [key, value] of Object.entries(activeFilters)) {
180+ if ((metadata[key] || '').toLowerCase() !== value.toLowerCase()) {
181+ metadataMatch = false;
182+ break;
183+ }
184+ }
185+
186+ const matches = textMatch && metadataMatch;
97187 card.style.display = matches ? '' : 'none';
98188 if (matches) visible++;
99189 });
100190
101191 countEl.textContent = `${visible} sample${visible !== 1 ? 's' : ''}`;
102192 noResults.classList.toggle('hidden', visible > 0);
103- });
193+ }
194+
195+ searchInput.addEventListener('input', applyFilters);
196+ filterSelects.forEach((select) => select.addEventListener('change', applyFilters));
104197</script >
0 commit comments