diff --git a/src/components/DropDownSelector.jsx b/src/components/DropDownSelector.jsx
index 439ca00..3f9a355 100644
--- a/src/components/DropDownSelector.jsx
+++ b/src/components/DropDownSelector.jsx
@@ -15,7 +15,7 @@ const AddButton = styled(Button)(({ theme }) => ({
borderRadius: '4px',
textTransform: 'none',
justifyContent: 'flex-start',
- width: '300px', // Increased width
+ width: '400px',
'&:hover': {
backgroundColor: '#1976d2',
},
@@ -26,7 +26,7 @@ const AddButton = styled(Button)(({ theme }) => ({
const StyledMenu = styled(Menu)(({ theme }) => ({
'& .MuiPaper-root': {
- width: '300px', // Match button width
+ width: '400px',
maxHeight: '240px',
marginTop: '4px',
borderRadius: '8px',
@@ -79,6 +79,14 @@ const DropDownSelector = ({ availableSelections, selections, setSelections, opti
// Filter out already selected items
const availableItems = availableSelections.filter((item) => !selections.includes(item));
+ // Trim the option name for better display
+ let trimmedOption = option;
+ if (option === 'Activities') {
+ trimmedOption = 'activity';
+ } else if (option.endsWith('s')) {
+ trimmedOption = option.slice(0, -1);
+ }
+
return (
+}
- sx={{ width: "100%" }}
+ sx={{ width: '100%' }}
>
-
- Add another
+
+ Add another {trimmedOption.toLowerCase()}
- {option}
200 ? description.substring(0, 200) + '...' : description;
+ const displayedActivities = practitioner.activities.slice(0, 3);
+
+ return (
+
+
+
+
+
+
+
+ {practitioner.org}
+
+
+
+ {truncatedDescription}
+
+
+
+
+ Adaptation Expertise
+
+
+ {displayedActivities.map((activity, index) => (
+
+ {activity}
+
+ ))}
+
+
+
+
+ }
+ sx={{
+ backgroundColor: theme.palette.primary.midBlue,
+ borderRadius: 8,
+ textTransform: 'none',
+ mb: 2,
+ '&:hover': {
+ backgroundColor: theme.palette.primary.main,
+ },
+ }}
+ >
+ Full Practitioner Org Profile
+
+
+
+ }
+ label="Compare this practitioner"
+ sx={{
+ '& .MuiFormControlLabel-label': {
+ fontSize: '0.875rem',
+ color: theme.palette.primary.main,
+ },
+ }}
+ />
+
+
+
+ );
+}
diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx
index 516c222..4ccbe51 100644
--- a/src/pages/LandingPage.jsx
+++ b/src/pages/LandingPage.jsx
@@ -1,83 +1,578 @@
-import { Box, Button, Container, ThemeProvider } from '@mui/material';
-import GroupIcon from '@mui/icons-material/Group';
-import BusinessIcon from '@mui/icons-material/Business';
-import SearchIcon from '@mui/icons-material/Search';
-import theme from '../theme';
+import React, { useState, useEffect } from 'react';
+import {
+ Autocomplete,
+ TextField,
+ Typography,
+ Container,
+ Box,
+ Paper,
+ Button,
+ Grid,
+ Collapse,
+ Chip,
+ Menu,
+ MenuItem,
+ ToggleButtonGroup,
+ ToggleButton,
+} from '@mui/material';
+import LocationOnIcon from '@mui/icons-material/LocationOn';
+import AddIcon from '@mui/icons-material/Add';
+import TuneIcon from '@mui/icons-material/Tune';
+import WindowIcon from '@mui/icons-material/Window';
+import CompareArrowsIcon from '@mui/icons-material/CompareArrows';
+import { fetchFilteredPractitioners, fetchOptionsFromAirtable, fetchAllPractitioners } from '../util/api';
+import PractitionerCard from '../components/PractitionerCard';
+
+const PRACTITIONERS_PER_PAGE = 6;
+
+// State capitals data
+const cityData = [
+ { city: 'Montgomery', state: 'Alabama' },
+ { city: 'Juneau', state: 'Alaska' },
+ { city: 'Phoenix', state: 'Arizona' },
+ { city: 'Little Rock', state: 'Arkansas' },
+ { city: 'Sacramento', state: 'California' },
+ { city: 'Denver', state: 'Colorado' },
+ { city: 'Hartford', state: 'Connecticut' },
+ { city: 'Dover', state: 'Delaware' },
+ { city: 'Tallahassee', state: 'Florida' },
+ { city: 'Atlanta', state: 'Georgia' },
+ { city: 'Honolulu', state: 'Hawaii' },
+ { city: 'Boise', state: 'Idaho' },
+ { city: 'Springfield', state: 'Illinois' },
+ { city: 'Indianapolis', state: 'Indiana' },
+ { city: 'Des Moines', state: 'Iowa' },
+ { city: 'Topeka', state: 'Kansas' },
+ { city: 'Frankfort', state: 'Kentucky' },
+ { city: 'Baton Rouge', state: 'Louisiana' },
+ { city: 'Augusta', state: 'Maine' },
+ { city: 'Annapolis', state: 'Maryland' },
+ { city: 'Boston', state: 'Massachusetts' },
+ { city: 'Lansing', state: 'Michigan' },
+ { city: 'Saint Paul', state: 'Minnesota' },
+ { city: 'Jackson', state: 'Mississippi' },
+ { city: 'Jefferson City', state: 'Missouri' },
+ { city: 'Helena', state: 'Montana' },
+ { city: 'Lincoln', state: 'Nebraska' },
+ { city: 'Carson City', state: 'Nevada' },
+ { city: 'Concord', state: 'New Hampshire' },
+ { city: 'Trenton', state: 'New Jersey' },
+ { city: 'Santa Fe', state: 'New Mexico' },
+ { city: 'Albany', state: 'New York' },
+ { city: 'Raleigh', state: 'North Carolina' },
+ { city: 'Bismarck', state: 'North Dakota' },
+ { city: 'Columbus', state: 'Ohio' },
+ { city: 'Oklahoma City', state: 'Oklahoma' },
+ { city: 'Salem', state: 'Oregon' },
+ { city: 'Harrisburg', state: 'Pennsylvania' },
+ { city: 'Providence', state: 'Rhode Island' },
+ { city: 'Columbia', state: 'South Carolina' },
+ { city: 'Pierre', state: 'South Dakota' },
+ { city: 'Nashville', state: 'Tennessee' },
+ { city: 'Austin', state: 'Texas' },
+ { city: 'Salt Lake City', state: 'Utah' },
+ { city: 'Montpelier', state: 'Vermont' },
+ { city: 'Richmond', state: 'Virginia' },
+ { city: 'Olympia', state: 'Washington' },
+ { city: 'Charleston', state: 'West Virginia' },
+ { city: 'Madison', state: 'Wisconsin' },
+ { city: 'Cheyenne', state: 'Wyoming' },
+];
+
+const FilterSection = ({ title, description, type, selected, availableOptions, onAdd, onRemove }) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+
+ const handleClick = (event) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const handleSelect = (option) => {
+ onAdd(option);
+ handleClose();
+ };
+
+ const getButtonText = () => {
+ switch (type) {
+ case 'activities':
+ return 'Add activity';
+ case 'hazards':
+ return 'Add hazard';
+ case 'sectors':
+ return 'Add sector';
+ default:
+ return 'Add';
+ }
+ };
+
+ // Filter out already selected options
+ const availableChoices = availableOptions.filter((option) => !selected.includes(option));
-export default function LandingPage() {
return (
-
-
+
- Welcome to CRF Matching Tool
-
+ {title}
+
+
+
+
+
+ {description}
+
+
+
+ {selected.map((item) => (
+ onRemove(item)}
sx={{
- backgroundColor: 'primary.main',
- p: 3,
- minWidth: '200px',
- textTransform: 'none',
- '&:hover': {
- backgroundColor: 'primary.dark',
+ borderRadius: '16px',
+ bgcolor: 'primary.tan',
+ '& .MuiChip-deleteIcon': {
+ color: 'primary.main',
},
}}
- >
- Communities
-
+ />
+ ))}
+
+ }
+ onClick={handleClick}
+ disabled={availableChoices.length === 0}
+ sx={{
+ bgcolor: 'grey.400',
+ color: 'white',
+ textTransform: 'none',
+ borderRadius: '20px',
+ '&:hover': {
+ bgcolor: 'grey.500',
+ },
+ '&.Mui-disabled': {
+ bgcolor: 'grey.300',
+ color: 'grey.500',
+ },
+ }}
+ >
+ {getButtonText()}
+
+
+
+
+
+ );
+};
+
+const ViewToggle = ({ view, onViewChange }) => {
+ return (
+
+ onViewChange(null, 'cards')}
+ >
+
+ Cards
+
+ onViewChange(null, 'compare')}
+ >
+
+ Compare
+
+
+ );
+};
+
+export default function LandingPage() {
+ const [selectedLocation, setSelectedLocation] = useState(null);
+ const [selectedState, setSelectedState] = useState('');
+ const [practitioners, setPractitioners] = useState([]);
+ const [totalPractitioners, setTotalPractitioners] = useState(0);
+ const [showFilters, setShowFilters] = useState(false);
+ const [currentView, setCurrentView] = useState('cards');
+ const [displayCount, setDisplayCount] = useState(PRACTITIONERS_PER_PAGE);
+ const [filters, setFilters] = useState({
+ activities: [],
+ sectors: [],
+ hazards: [],
+ size: [],
+ });
+ const [availableOptions, setAvailableOptions] = useState({
+ activities: [],
+ sectors: [],
+ hazards: [],
+ size: [],
+ });
+
+ const visiblePractitioners = practitioners.slice(0, displayCount);
+ const hasMorePractitioners = practitioners.length > displayCount;
+ const hasAnyFilters = Object.values(filters).some((arr) => arr.length > 0) || selectedState;
+
+ useEffect(() => {
+ fetchOptionsFromAirtable(setAvailableOptions);
+ }, []);
+
+ useEffect(() => {
+ // Get total practitioners count
+ fetchAllPractitioners((practitioners) => {
+ setTotalPractitioners(practitioners.length);
+ });
+ }, []);
+
+ useEffect(() => {
+ if (selectedState || Object.values(filters).some((arr) => arr.length > 0)) {
+ fetchFilteredPractitioners(
+ {
+ state: selectedState ? [selectedState] : [],
+ ...filters,
+ },
+ setPractitioners
+ );
+ } else {
+ setPractitioners([]);
+ }
+ }, [selectedState, filters]);
+
+ const handleLocationSelect = (event, newValue) => {
+ setSelectedLocation(newValue);
+ if (newValue) {
+ setSelectedState(newValue.state);
+ } else {
+ setSelectedState('');
+ }
+ };
+
+ const handleAddFilter = (category, value) => {
+ setFilters((prev) => ({
+ ...prev,
+ [category]: [...prev[category], value],
+ }));
+ };
+
+ const handleRemoveFilter = (category, itemToRemove) => {
+ setFilters((prev) => ({
+ ...prev,
+ [category]: prev[category].filter((item) => item !== itemToRemove),
+ }));
+ };
+
+ const handleViewChange = (event, newView) => {
+ if (newView !== null) {
+ setCurrentView(newView);
+ }
+ };
+
+ return (
+
+
+
+ Looking to connect to an adaptation practitioner?
+
- }
+
+ {/* Location Search Row */}
+
- Find a Practitioner for Your Community
-
+
+ Where is your community?
+
+
+ `${option.city}, ${option.state}`}
+ sx={{ flexGrow: 1 }}
+ renderInput={(params) => (
+ ,
+ }}
+ />
+ )}
+ />
- }
+ {selectedLocation && (
+ }
+ onClick={() => {
+ setSelectedLocation(null);
+ setSelectedState('');
+ }}
+ sx={{
+ bgcolor: 'grey.400',
+ textTransform: 'none',
+ whiteSpace: 'nowrap',
+ '&:hover': {
+ bgcolor: 'grey.500',
+ },
+ }}
+ >
+ Change your community
+
+ )}
+
+
+ {/* Filter Toggle */}
+ setShowFilters(!showFilters)}
>
- Practitioners
-
-
-
-
+
+
+ Filter practitioners by their expertise
+
+
+
+ {/* Filter Sections */}
+
+
+ handleAddFilter('activities', value)}
+ onRemove={(value) => handleRemoveFilter('activities', value)}
+ />
+
+ handleAddFilter('hazards', value)}
+ onRemove={(value) => handleRemoveFilter('hazards', value)}
+ />
+
+ handleAddFilter('sectors', value)}
+ onRemove={(value) => handleRemoveFilter('sectors', value)}
+ />
+
+
+
+
+ {/* Practitioners Section */}
+ {practitioners.length > 0 && hasAnyFilters && (
+
+
+ Adaptation practitioners that can help your community
+
+
+
+
+
+ {visiblePractitioners.length} out of {practitioners.length} practitioners selected from the{' '}
+ {totalPractitioners} available in the practitioner registry
+
+
+
+ {visiblePractitioners.map((practitioner, index) => (
+
+
+
+ ))}
+
+
+ {hasMorePractitioners && (
+
+ setDisplayCount((prev) => prev + PRACTITIONERS_PER_PAGE)}
+ variant="outlined"
+ sx={{
+ color: 'text.primary',
+ backgroundColor: 'white',
+ border: '1px solid',
+ borderColor: 'grey.300',
+ textTransform: 'none',
+ boxShadow: 1,
+ px: 4,
+ py: 1,
+ '&:hover': {
+ backgroundColor: 'grey.50',
+ borderColor: 'grey.400',
+ },
+ }}
+ >
+ Load more practitioners
+
+
+ )}
+
+ )}
+
+
);
}
diff --git a/src/pages/OldLandingPage.jsx b/src/pages/OldLandingPage.jsx
new file mode 100644
index 0000000..4d7830f
--- /dev/null
+++ b/src/pages/OldLandingPage.jsx
@@ -0,0 +1,83 @@
+import { Box, Button, Container, ThemeProvider } from '@mui/material';
+import GroupIcon from '@mui/icons-material/Group';
+import BusinessIcon from '@mui/icons-material/Business';
+import SearchIcon from '@mui/icons-material/Search';
+import theme from '../theme';
+
+export default function OldLandingPage() {
+ return (
+
+
+ Welcome to CRF Matching Tool
+
+ }
+ sx={{
+ backgroundColor: 'primary.main',
+ p: 3,
+ minWidth: '200px',
+ textTransform: 'none',
+ '&:hover': {
+ backgroundColor: 'primary.dark',
+ },
+ }}
+ >
+ Communities
+
+
+ }
+ sx={{
+ backgroundColor: 'primary.main',
+ p: 3,
+ minWidth: '200px',
+ textTransform: 'none',
+ '&:hover': {
+ backgroundColor: 'primary.dark',
+ },
+ }}
+ >
+ Find a Practitioner for Your Community
+
+
+ }
+ sx={{
+ backgroundColor: 'primary.main',
+ p: 3,
+ minWidth: '200px',
+ textTransform: 'none',
+ '&:hover': {
+ backgroundColor: 'primary.dark',
+ },
+ }}
+ >
+ Practitioners
+
+
+
+
+ );
+}
diff --git a/src/pages/PractitionerListPage.jsx b/src/pages/PractitionerListPage.jsx
index c2af659..af2dd3c 100644
--- a/src/pages/PractitionerListPage.jsx
+++ b/src/pages/PractitionerListPage.jsx
@@ -1,125 +1,12 @@
-import { useState, useEffect } from 'react';
-import { Box, Button, Card, CardContent, Chip, Container, Grid, Stack, Typography } from '@mui/material';
-import PersonIcon from '@mui/icons-material/Person';
+import React, { useState, useEffect } from 'react';
+import { Container, Grid, Typography } from '@mui/material';
import { fetchAllPractitioners } from '../util/api';
import FullPageSpinner from '../components/FullPageSpinner';
+import PractitionerCard from '../components/PractitionerCard';
import { ThemeProvider } from '@mui/material/styles';
-import climatePracLogo from '../assets/climate_prac.png';
import theme from '../theme';
-function PractitionerCard({ practitioner }) {
- const description = practitioner.info || 'No description available';
- const truncatedDescription = description.length > 200 ? description.substring(0, 200) + '...' : description;
-
- // Only take first 3 activities
- const displayedActivities = practitioner.activities.slice(0, 3);
-
- return (
-
-
- {/* Logo Container */}
-
-
-
-
-
- {practitioner.org}
-
-
-
- {truncatedDescription}
-
-
-
-
- Expertise
-
-
- {displayedActivities.map((activity, index) => (
-
- ))}
-
-
-
-
- }
- sx={{
- backgroundColor: theme.palette.primary.midBlue,
- borderRadius: 8,
- textTransform: 'none',
- '&:hover': {
- backgroundColor: theme.palette.primary.main,
- },
- }}
- >
- Full Practitioner Org Profile
-
-
-
-
- );
-}
-
-function PractitionerListPage() {
+export default function PractitionerListPage() {
const [allPractitioners, setAllPractitioners] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
@@ -136,13 +23,8 @@ function PractitionerListPage() {
})();
}, []);
- if (isLoading) {
- return ;
- }
-
- if (error) {
- return Error: {error.message}
;
- }
+ if (isLoading) return ;
+ if (error) return Error: {error.message}
;
return (
@@ -178,5 +60,3 @@ function PractitionerListPage() {
);
}
-
-export default PractitionerListPage;
diff --git a/src/util/api.js b/src/util/api.js
index b0061c0..03b64a1 100644
--- a/src/util/api.js
+++ b/src/util/api.js
@@ -35,7 +35,7 @@ const practitionerFieldMap = {
name: 'Name',
org: 'Organization Name',
website: 'Organization Website',
- // TODO setup
+ status: 'Status',
linkedIn: 'LinkedIn URL',
email: 'Email',
phone: 'Phone Number',
@@ -80,6 +80,61 @@ export const fetchPractitioner = (practitionerId, setPractitioner) => {
});
};
+export const fetchFilteredPractitioners = (filters, setPractitioners) => {
+ // If no filters, return empty array
+ if (!Object.values(filters).some((val) => val && val.length)) {
+ setPractitioners([]);
+ return;
+ }
+
+ base('Practitioner')
+ .select({
+ view: 'Grid view',
+ fields: practFetchFields,
+ // Sort by organization name
+ sort: [{ field: 'Organization Name', direction: 'asc' }],
+ })
+ .eachPage(
+ function page(records, fetchNextPage) {
+ const recs = records
+ .map((rawRec) => rawRec.fields)
+ .map((rec) => normalizeRec(rec, practitionerFieldMap))
+ // Only include Accepted practitioners
+ .filter((rec) => rec.status === 'Accepted')
+ // Filter based on provided criteria
+ .filter((rec) => {
+ let matches = true;
+
+ if (filters.state?.length) {
+ matches = matches && rec.state.some((s) => filters.state.includes(s));
+ }
+ if (filters.activities?.length) {
+ matches = matches && rec.activities.some((a) => filters.activities.includes(a));
+ }
+ if (filters.sectors?.length) {
+ matches = matches && rec.sectors.some((s) => filters.sectors.includes(s));
+ }
+ if (filters.hazards?.length) {
+ matches = matches && rec.hazards.some((h) => filters.hazards.includes(h));
+ }
+ if (filters.size?.length) {
+ matches = matches && rec.size.some((s) => filters.size.includes(s));
+ }
+
+ return matches;
+ });
+
+ setPractitioners(recs);
+ },
+ function done(err) {
+ if (err) {
+ console.error(err);
+ return;
+ }
+ }
+ );
+};
+
export const fetchCommunity = (communityId, setCommunity) => {
base('Community')
.select({
@@ -140,7 +195,11 @@ export const fetchAllPractitioners = (setAllPractitioners) => {
})
.eachPage(
function page(records, fetchNextPage) {
- const recs = records.map((rawRec) => rawRec.fields).map((rec) => normalizeRec(rec, practitionerFieldMap));
+ const recs = records
+ .map((rawRec) => rawRec.fields)
+ .map((rec) => normalizeRec(rec, practitionerFieldMap))
+ // Only include Accepted practitioners
+ .filter((rec) => rec.status === 'Accepted');
practitioners.push(...recs);
fetchNextPage();
},
@@ -283,9 +342,6 @@ export const fetchPractitionersByFilters = (selectedOptions, setPractitioners) =
return;
}
- // List of practitioners to exclude
- const excludedPractitioners = ['NEMAC', 'NEMAC 2', 'NEMAC 3'];
-
base('Practitioner')
.select({
view: 'Grid view',
@@ -301,8 +357,6 @@ export const fetchPractitionersByFilters = (selectedOptions, setPractitioners) =
const recs = records
.map((rawRec) => rawRec.fields)
.map((rec) => normalizeRec(rec, practitionerFieldMap))
- // Filter out excluded practitioners
- .filter((rec) => !excludedPractitioners.includes(rec.org))
// Calculate match score based on count of all matching items
.map((rec) => {
let matchCount = 0;