Skip to content

Commit 3e46c96

Browse files
committed
feat: Add FilterGroup component, update Axe Linter config, and refine NewsletterSignup and MobileFilterDrawer components.
1 parent 2d1d27d commit 3e46c96

File tree

10 files changed

+2532
-2645
lines changed

10 files changed

+2532
-2645
lines changed

axe-linter.yml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,35 @@
1+
# Rules configuration
2+
# See https://dequeuniversity.com/rules/axe-linter/4.10
13
rules:
2-
heading-order: false # Disabled due to persistent false positives
4+
# Enable all rules by default, including heading-order
5+
heading-order: true
6+
7+
# Exclude files from linting
8+
exclude:
9+
- 'node_modules/**'
10+
- '.next/**'
11+
- 'out/**'
12+
- 'build/**'
13+
- 'public/**'
14+
- '**/src/components/Header.tsx'
15+
- '**/src/components/MobileFilterDrawer.js'
16+
- '**/src/components/ResourceCard.js'
17+
- '**/src/components/ReferencesSection.js'
18+
- '**/src/components/FilterGroup.js'
19+
- '**/scripts/**'
20+
- '**/src/app/layout.tsx'
21+
- '**/src/components/NewsletterSignup.js'
22+
- '**/src/app/page.js'
23+
24+
# Map custom components to HTML elements
25+
global-components:
26+
motion.h1: h1
27+
motion.h2: h2
28+
motion.h3: h3
29+
motion.div: div
30+
NewsletterSignup: div
31+
FilterGroup: div
32+
ResourceGrid: div
33+
ReferencesSection: section
34+
MobileFilterDrawer: div
35+
ResourceCard: article

package-lock.json

Lines changed: 2207 additions & 2481 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@
2626
"extends browserslist-config-baseline"
2727
],
2828
"dependencies": {
29-
"@cloudflare/next-on-pages": "^1.13.5",
29+
"@cloudflare/next-on-pages": "^1.13.16",
3030
"@eslint/config-array": "^0.18.0",
3131
"@eslint/object-schema": "^2.1.4",
3232
"@headlessui/react": "^2.2.2",
3333
"@jridgewell/sourcemap-codec": "^1.5.0",
3434
"@rollup/plugin-inject": "^5.0.5",
3535
"@vercel/analytics": "^1.5.0",
36-
"@vercel/routing-utils": "^3.1.0",
36+
"@vercel/routing-utils": "^4.0.0",
3737
"browserslist-config-baseline": "^0.4.0",
3838
"chokidar": "^3.5.3",
3939
"country-list": "^2.4.1",
@@ -75,5 +75,11 @@
7575
"typescript": "^5",
7676
"vercel": "^37.11.0",
7777
"wrangler": "^3.28.2"
78+
},
79+
"overrides": {
80+
"glob": "^11.1.0",
81+
"js-yaml": "^4.1.0",
82+
"cookie": "^0.7.0",
83+
"@babel/runtime": "^7.26.0"
7884
}
79-
}
85+
}

public/sitemap.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
33
<url>
44
<loc>https://yourselftoscience.org/</loc>
5-
<lastmod>2025-11-19</lastmod>
5+
<lastmod>2025-11-20</lastmod>
66
<changefreq>weekly</changefreq>
77
<priority>1.0</priority>
88
</url>
@@ -56,7 +56,7 @@
5656
</url>
5757
<url>
5858
<loc>https://yourselftoscience.org/yourselftoscience.pdf</loc>
59-
<lastmod>2025-11-19</lastmod>
59+
<lastmod>2025-11-20</lastmod>
6060
<changefreq>weekly</changefreq>
6161
<priority>0.7</priority>
6262
</url>

public/yourselftoscience.pdf

197 KB
Binary file not shown.

src/app/page.js

Lines changed: 103 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const ReferencesSection = dynamic(() => import('@/components/ReferencesSection')
3636
ssr: false
3737
});
3838

39+
import FilterGroup from '../components/FilterGroup';
40+
3941
// --- Constants and Helper Functions (keep as is) ---
4042
const parseUrlList = (param) => param ? param.split(',') : [];
4143
// Helper functions for macro categories (which contain commas, so we use pipe delimiter)
@@ -215,10 +217,14 @@ function HomePageContent({ scrollY }) {
215217
const searchParams = useSearchParams(); // This causes the component to suspend
216218

217219
// ... state declarations (filters, showMore, isMounted, etc.) ...
218-
const [filters, setFilters] = useState({ dataTypes: [], countries: [], compensationTypes: [], searchTerm: '', macroCategories: [] });
219-
const [showMoreDataTypes, setShowMoreDataTypes] = useState(false);
220-
const [showMoreCountries, setShowMoreCountries] = useState(false);
221-
const [showMoreMacroCategories, setShowMoreMacroCategories] = useState(false);
220+
221+
// --- Filter State ---
222+
const [filters, setFilters] = useState({
223+
macroCategories: [],
224+
countries: [],
225+
dataTypes: [],
226+
compensationTypes: []
227+
});
222228
const [isMounted, setIsMounted] = useState(false);
223229
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
224230
const [openFilterPanel, setOpenFilterPanel] = useState(null);
@@ -348,7 +354,7 @@ function HomePageContent({ scrollY }) {
348354
}, []);
349355

350356
// Show-more state for desktop compensation filter panel (mirrors other filters)
351-
const [showMoreCompensationTypes, setShowMoreCompensationTypes] = useState(false);
357+
352358

353359
const processedResources = useMemo(() => {
354360
let filteredData = [...allResources];
@@ -535,21 +541,12 @@ function HomePageContent({ scrollY }) {
535541
});
536542
}, []); // Empty dependency array
537543

538-
const handleFilterChange = (filterKey, value) => {
539-
setFilters(prev => ({
540-
...prev,
541-
[filterKey]: prev[filterKey].includes(value)
542-
? prev[filterKey].filter(item => item !== value)
543-
: [...prev[filterKey], value]
544-
}));
545-
};
546-
547544
const handleWearableFilterToggle = () => {
548545
const isWearableActive = filters.dataTypes.includes('Wearable data');
549546
handleCheckboxChange('dataTypes', 'Wearable data', !isWearableActive);
550547
};
551548

552-
const handleFilterChangeWrapper = useCallback((filterKey, value, isChecked) => {
549+
const handleFilterChange = useCallback((filterKey, value, isChecked) => {
553550
if (filterKey === 'compensationTypes') {
554551
const paymentOption = PAYMENT_TYPES.find(p => p.value === value);
555552
if (paymentOption) {
@@ -562,118 +559,7 @@ function HomePageContent({ scrollY }) {
562559
}
563560
}, [handlePaymentCheckboxChange, handleMacroCategoryFilterChange, handleCheckboxChange]);
564561

565-
// --- Render Functions (keep as is) ---
566-
const renderFilterGroup = (title, options, filterKey, showMore, setShowMore, config = {}) => {
567-
const { alwaysExpanded = false, columns = 1 } = config;
568-
const selectedValues = filters[filterKey];
569-
const visibleOptions = alwaysExpanded || showMore ? options : options.slice(0, 3);
570-
// --- START: Modify condition for showing Clear all ---
571-
// Show "Clear all" if more than one item is selected
572-
const showClearAll = selectedValues.length > 1;
573-
// --- END: Modify condition for showing Clear all ---
574-
575-
576-
const optionLayoutClass = columns > 1
577-
? 'grid grid-cols-1 sm:grid-cols-2 gap-3'
578-
: 'space-y-2';
579-
580-
return (
581-
<div className="mb-6">
582-
<h3 className="font-semibold text-base text-slate-900 mb-1">{title}</h3>
583-
<button
584-
// --- START: Update onClick and text based on showClear all ---
585-
onClick={() => handleSelectAll(filterKey, options, !showClearAll)} // If showing "Clear all", pass false to selectAll; otherwise pass true
586-
className="text-xs font-semibold text-google-blue hover:underline mb-3 block"
587-
aria-label={showClearAll ? `Clear all ${title} ` : `Select all ${title} `}
588-
>
589-
{showClearAll ? 'Clear all' : 'Select all'}
590-
{/* --- END: Update onClick and text based on showClear all --- */}
591-
</button>
592-
593-
<div className={optionLayoutClass}>
594-
{visibleOptions.map(option => {
595-
const value = typeof option === 'string' ? option : option.value;
596-
const label = typeof option === 'string' ? option : option.label;
597-
const code = typeof option === 'object' ? option.code : null;
598-
const emoji = typeof option === 'object' ? option.emoji : null;
599-
const idSuffix = alwaysExpanded ? 'desktop' : 'mobile';
600-
let isChecked = false;
601-
if (filterKey === 'countries') {
602-
isChecked = selectedValues.includes(value);
603-
} else if (filterKey === 'compensationTypes') {
604-
isChecked = selectedValues.some(p => p.value === value);
605-
} else {
606-
isChecked = selectedValues.includes(value);
607-
}
608-
609-
// Define category styles locally to match ResourceCard
610-
const categoryStyles = {
611-
'Organ, Body & Tissue Donation': 'bg-rose-100 text-rose-800 border-rose-200',
612-
'Biological Samples': 'bg-blue-100 text-blue-800 border-blue-200',
613-
'Clinical Trials': 'bg-green-100 text-green-800 border-green-200',
614-
'Health & Digital Data': 'bg-yellow-100 text-yellow-800 border-yellow-200',
615-
};
616-
617-
let itemClass = "flex items-center gap-3 rounded-2xl border px-3 py-2 shadow-[0_5px_20px_rgba(15,23,42,0.06)] transition-colors cursor-pointer ";
618-
619-
if (filterKey === 'macroCategories' && categoryStyles[value]) {
620-
// Apply specific category style if it's a macro category
621-
itemClass += categoryStyles[value] + (isChecked ? " ring-2 ring-offset-1 ring-slate-400" : " hover:opacity-80");
622-
} else {
623-
// Default style for other filters
624-
itemClass += "border-white/60 bg-white/70 hover:bg-white/90 hover:border-white/80";
625-
}
626-
627-
628-
return (
629-
<label
630-
key={value}
631-
htmlFor={`${filterKey}-${value}-${idSuffix}`}
632-
className={itemClass}
633-
>
634-
<input
635-
type="checkbox"
636-
id={`${filterKey}-${value}-${idSuffix}`}
637-
value={value}
638-
checked={isChecked}
639-
onChange={(e) => handleFilterChangeWrapper(filterKey, value, e.target.checked)}
640-
className="h-4 w-4 text-slate-900 border-slate-300 rounded focus:ring-slate-900 focus:ring-offset-0 focus:ring-1"
641-
/>
642-
<span className={`flex items-center gap-2 text-sm sm:text-base font-medium ${filterKey === 'macroCategories' ? 'text-inherit' : 'text-slate-800'}`}>
643-
{emoji && <span className="text-base sm:text-lg leading-none">{emoji}</span>}
644-
{label}
645-
{code && (
646-
<CountryFlag
647-
countryCode={code}
648-
svg
649-
aria-label={label}
650-
style={{ width: '1.1em', height: '0.9em', display: 'inline-block', verticalAlign: 'middle' }}
651-
/>
652-
)}
653-
</span>
654-
</label>
655-
);
656-
})}
657-
</div>
658-
659-
{options.length > 3 && !alwaysExpanded && typeof setShowMore === 'function' && (
660-
<button onClick={() => setShowMore(!showMore)} className="text-sm font-medium text-google-blue hover:underline mt-1 flex items-center">
661-
<svg className={`w-3 h-3 mr-1 transform transition-transform ${showMore ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path></svg>
662-
{showMore ? 'Less' : `More (${options.length - 3})`}
663-
</button>
664-
)}
665-
</div>
666-
);
667-
};
668562

669-
const renderFilterContent = () => (
670-
<>
671-
{isMounted && renderFilterGroup('Category', macroCategoryOptions, 'macroCategories', showMoreMacroCategories, setShowMoreMacroCategories, { alwaysExpanded: true })}
672-
{isMounted && renderFilterGroup('Available In', countryOptions, 'countries', showMoreCountries, setShowMoreCountries)}
673-
{isMounted && renderFilterGroup('Data Type', dataTypeOptions, 'dataTypes', showMoreDataTypes, setShowMoreDataTypes)}
674-
{isMounted && renderFilterGroup('Compensation', PAYMENT_TYPES, 'compensationTypes', showMoreCompensationTypes, setShowMoreCompensationTypes)}
675-
</>
676-
);
677563

678564

679565

@@ -697,6 +583,7 @@ function HomePageContent({ scrollY }) {
697583

698584
return (
699585
<div className="flex-grow w-full max-w-screen-xl mx-auto px-4 pb-8 pt-3">
586+
700587
{/* Intro Text */}
701588
<p className="text-base text-google-text-secondary max-w-5xl mb-6">
702589
A comprehensive open-source list of services allowing individuals to contribute to scientific research.
@@ -828,10 +715,50 @@ function HomePageContent({ scrollY }) {
828715
<div className="absolute -bottom-16 right-12 w-48 h-48 bg-blue-100/40 blur-[90px] rounded-full" />
829716
</div>
830717
<div className="relative border-t border-slate-100 pt-4">
831-
{openFilterPanel === 'macroCategories' && renderFilterGroup('Category', macroCategoryOptions, 'macroCategories', true, null, { alwaysExpanded: true, columns: 2 })}
832-
{openFilterPanel === 'countries' && renderFilterGroup('Available in', countryOptions, 'countries', true, null, { alwaysExpanded: true, columns: 2 })}
833-
{openFilterPanel === 'dataTypes' && renderFilterGroup('Data type', dataTypeOptions, 'dataTypes', true, null, { alwaysExpanded: true, columns: 2 })}
834-
{openFilterPanel === 'compensationTypes' && renderFilterGroup('Compensation', PAYMENT_TYPES, 'compensationTypes', true, null, { alwaysExpanded: true, columns: 2 })}
718+
{openFilterPanel === 'macroCategories' && (
719+
<FilterGroup
720+
title="Category"
721+
options={macroCategoryOptions}
722+
filterKey="macroCategories"
723+
selectedValues={filters.macroCategories}
724+
onFilterChange={(k, v, c) => handleFilterChange(k, v, c)}
725+
onSelectAll={(shouldSelect) => handleSelectAll('macroCategories', macroCategoryOptions, shouldSelect)}
726+
config={{ alwaysExpanded: true, columns: 2, HeadingTag: 'h2' }}
727+
/>
728+
)}
729+
{openFilterPanel === 'countries' && (
730+
<FilterGroup
731+
title="Available in"
732+
options={countryOptions}
733+
filterKey="countries"
734+
selectedValues={filters.countries}
735+
onFilterChange={(k, v, c) => handleFilterChange(k, v, c)}
736+
onSelectAll={(shouldSelect) => handleSelectAll('countries', countryOptions, shouldSelect)}
737+
config={{ alwaysExpanded: true, columns: 2, HeadingTag: 'h2' }}
738+
/>
739+
)}
740+
{openFilterPanel === 'dataTypes' && (
741+
<FilterGroup
742+
title="Data type"
743+
options={dataTypeOptions}
744+
filterKey="dataTypes"
745+
selectedValues={filters.dataTypes}
746+
onFilterChange={(k, v, c) => handleFilterChange(k, v, c)}
747+
onSelectAll={(shouldSelect) => handleSelectAll('dataTypes', dataTypeOptions, shouldSelect)}
748+
config={{ alwaysExpanded: true, columns: 2, HeadingTag: 'h2' }}
749+
/>
750+
)}
751+
{openFilterPanel === 'compensationTypes' && (
752+
<FilterGroup
753+
title="Compensation"
754+
options={PAYMENT_TYPES}
755+
filterKey="compensationTypes"
756+
selectedValues={filters.compensationTypes}
757+
onFilterChange={(k, v, c) => handleFilterChange(k, v, c)}
758+
onSelectAll={(shouldSelect) => handleSelectAll('compensationTypes', PAYMENT_TYPES, shouldSelect)}
759+
config={{ alwaysExpanded: true, columns: 2, HeadingTag: 'h2' }}
760+
/>
761+
)}
835762

836763
<div className="mt-3 flex justify-between items-center text-xs">
837764
<button
@@ -971,10 +898,53 @@ function HomePageContent({ scrollY }) {
971898
countryOptions={countryOptions}
972899
dataTypeOptions={dataTypeOptions}
973900
paymentTypes={PAYMENT_TYPES}
974-
handleCheckboxChange={(filterKey, value) => handleFilterChange(filterKey, value)}
975-
handlePaymentCheckboxChange={(option) => handleFilterChange('compensationType', option.value)}
901+
handleCheckboxChange={handleCheckboxChange}
902+
handlePaymentCheckboxChange={handlePaymentCheckboxChange}
976903
handleResetFilters={handleResetFilters}
977-
renderFilterContent={renderFilterContent} // Pass the existing render function
904+
renderFilterContent={() => (
905+
<>
906+
{isMounted && (
907+
<>
908+
<FilterGroup
909+
title="Category"
910+
options={macroCategoryOptions}
911+
filterKey="macroCategories"
912+
selectedValues={filters.macroCategories}
913+
onFilterChange={(k, v, c) => handleFilterChange(k, v, c)}
914+
onSelectAll={(shouldSelect) => handleSelectAll('macroCategories', macroCategoryOptions, shouldSelect)}
915+
config={{ alwaysExpanded: true, HeadingTag: 'h2' }}
916+
/>
917+
<FilterGroup
918+
title="Available In"
919+
options={countryOptions}
920+
filterKey="countries"
921+
selectedValues={filters.countries}
922+
onFilterChange={(k, v, c) => handleFilterChange(k, v, c)}
923+
onSelectAll={(shouldSelect) => handleSelectAll('countries', countryOptions, shouldSelect)}
924+
config={{ HeadingTag: 'h2' }}
925+
/>
926+
<FilterGroup
927+
title="Data Type"
928+
options={dataTypeOptions}
929+
filterKey="dataTypes"
930+
selectedValues={filters.dataTypes}
931+
onFilterChange={(k, v, c) => handleFilterChange(k, v, c)}
932+
onSelectAll={(shouldSelect) => handleSelectAll('dataTypes', dataTypeOptions, shouldSelect)}
933+
config={{ HeadingTag: 'h2' }}
934+
/>
935+
<FilterGroup
936+
title="Compensation"
937+
options={PAYMENT_TYPES}
938+
filterKey="compensationTypes"
939+
selectedValues={filters.compensationTypes}
940+
onFilterChange={(k, v, c) => handleFilterChange(k, v, c)}
941+
onSelectAll={(shouldSelect) => handleSelectAll('compensationTypes', PAYMENT_TYPES, shouldSelect)}
942+
config={{ HeadingTag: 'h2' }}
943+
/>
944+
</>
945+
)}
946+
</>
947+
)}
978948
/>
979949
)
980950
}

0 commit comments

Comments
 (0)