From eb065c2cdd24708f166740dd59278df2b93d5419 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Fri, 6 Dec 2024 11:19:51 -0500 Subject: [PATCH 01/45] removed PersonIcon svg and import from MUI --- src/components/ProfilePopper.jsx | 24 +++--------------------- src/components/svg/PersonIcon.jsx | 29 ----------------------------- 2 files changed, 3 insertions(+), 50 deletions(-) delete mode 100644 src/components/svg/PersonIcon.jsx diff --git a/src/components/ProfilePopper.jsx b/src/components/ProfilePopper.jsx index 9bcdf5a..1fead4c 100644 --- a/src/components/ProfilePopper.jsx +++ b/src/components/ProfilePopper.jsx @@ -1,8 +1,8 @@ import Popper from '@mui/material/Popper'; import ContactRow from './ContactRow'; import CloseIcon from '@mui/icons-material/Close'; +import PersonIcon from '@mui/icons-material/Person'; import { IconButton, Typography, Box, Stack, Button, ClickAwayListener } from '@mui/material'; -import PersonIcon from './svg/PersonIcon'; import theme from '../theme'; export default function ProfilePopper({ practitioner, poppedPractitioner, setPoppedPractitioner, headerRef }) { @@ -29,25 +29,7 @@ export default function ProfilePopper({ practitioner, poppedPractitioner, setPop }, }} > - - - + {/* content */} @@ -138,7 +120,7 @@ export default function ProfilePopper({ practitioner, poppedPractitioner, setPop {/* link to full profile */} - - - - - -} - From 0834e693aab9e2f2e89505f1dc78069f0df258fe Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Fri, 6 Dec 2024 11:23:17 -0500 Subject: [PATCH 02/45] removed unused svg and moved to MUI icons --- src/components/svg/GradCapIcon.jsx | 28 ---- src/components/svg/OpenBook.jsx | 15 -- src/pages/PractitionerPage.jsx | 232 ++++++++++++++--------------- 3 files changed, 116 insertions(+), 159 deletions(-) delete mode 100644 src/components/svg/GradCapIcon.jsx delete mode 100644 src/components/svg/OpenBook.jsx diff --git a/src/components/svg/GradCapIcon.jsx b/src/components/svg/GradCapIcon.jsx deleted file mode 100644 index 01376ac..0000000 --- a/src/components/svg/GradCapIcon.jsx +++ /dev/null @@ -1,28 +0,0 @@ - -import theme from '../../theme'; -import { SvgIcon } from '@mui/material'; - -export default function GradCapIcon() { - return ( - - - - - - ) -} \ No newline at end of file diff --git a/src/components/svg/OpenBook.jsx b/src/components/svg/OpenBook.jsx deleted file mode 100644 index 7d2639d..0000000 --- a/src/components/svg/OpenBook.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import theme from '../../theme'; -import { SvgIcon } from '@mui/material'; - -export default function OpenBookIcon() { - return ( - - - - - - ) -} diff --git a/src/pages/PractitionerPage.jsx b/src/pages/PractitionerPage.jsx index 83652a7..c13633e 100644 --- a/src/pages/PractitionerPage.jsx +++ b/src/pages/PractitionerPage.jsx @@ -1,101 +1,104 @@ -import { useState, useLayoutEffect } from "react"; -import { useParams } from "react-router-dom"; -import { ThemeProvider } from "@mui/material/styles"; -import { CssBaseline, Stack, Container, Box, Typography, styled } from '@mui/material' +import { useState, useLayoutEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { ThemeProvider } from '@mui/material/styles'; +import SchoolIcon from '@mui/icons-material/School'; +import { CssBaseline, Stack, Container, Box, Typography, styled } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import theme from '../theme'; import FullPageSpinner from '../components/FullPageSpinner'; -import GradCapSvg from '../components/svg/GradCapIcon'; import ContactRow from '../components/ContactRow'; // API -import { fetchPractitioner } from '../util/api' - +import { fetchPractitioner } from '../util/api'; const sections = [ { - title: "Where we work", - objKey: "state", + title: 'Where we work', + objKey: 'state', }, { - title: "Activities we have expertise with", - objKey: "activities", + title: 'Activities we have expertise with', + objKey: 'activities', }, { - title: "Sectors we have expertise with", - objKey: "sectors", + title: 'Sectors we have expertise with', + objKey: 'sectors', }, { - title: "Hazards we have expertise with", - objKey: "hazards", + title: 'Hazards we have expertise with', + objKey: 'hazards', }, { - title: "Size of communities we have expertise with", - objKey: "size", - } -] - - + title: 'Size of communities we have expertise with', + objKey: 'size', + }, +]; function SectionHeader({ title, style }) { return ( - { title } - ) + > + {title} + + ); } -function StrTrainedRow ( { isTrained }) { +function StrTrainedRow({ isTrained }) { if (!isTrained) { - return 'No certifications' + return 'No certifications'; } return ( - + STR Training Class Completed + > + STR Training Class Completed + - ) + ); } function MatchBadge({ label, key }) { - return { label } + return ( + + {label} + + ); } function MatchSection({ practitioner, title, objKey }) { - const matchBadges = practitioner[objKey].map((label, index) => { - return MatchBadge({ label, key: index }) - }) + return MatchBadge({ label, key: index }); + }); return ( - + - { matchBadges } + {matchBadges} - ) + ); } const ContactAndTrainingBox = styled(Grid)(({ theme }) => ({ @@ -117,27 +120,30 @@ const ContactAndTrainingBox = styled(Grid)(({ theme }) => ({ margin: theme.spacing(1), })); - function PractitionerPageLoaded({ practitioner }) { - return ( - - - { /* Header */ } + + {/* Header */} { practitioner.org } + }} + > + {practitioner.org} - { /* Contact & Training Row */ } - - - { /* Contact */ } + {/* Contact & Training Row */} + + {/* Contact */} - + - - - - + + + + - + - { /* Training */ } + {/* Training */} - - + - - + - { practitioner.info || 'N/A' } + {practitioner.info || 'N/A'} - + - { practitioner.exampleStakeholders || 'N/A' } + {practitioner.exampleStakeholders || 'N/A'} - + - { practitioner.exampleMultipleBenefits || 'N/A' } + {practitioner.exampleMultipleBenefits || 'N/A'} - { - sections.map((data, index) => { - return { + return ( + - }) - } - + ); + })} - ) + ); } /// Practitioner Page /// function PractitionerPage() { + const { practitionerId } = useParams(); - const { practitionerId } = useParams() - - const [ practitioner, setPractitioner ] = useState(null) + const [practitioner, setPractitioner] = useState(null); useLayoutEffect(() => { - fetchPractitioner(practitionerId, setPractitioner) - }, []) + fetchPractitioner(practitionerId, setPractitioner); + }, []); if (practitioner) { - return ( - - ) + return ; } else { - return ( - - ) + return ; } - } -export default PractitionerPage \ No newline at end of file +export default PractitionerPage; From 218251be6a79630bc48e7a7ada84e5ce3e25de8e Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Fri, 6 Dec 2024 13:24:14 -0500 Subject: [PATCH 03/45] bringing in the compare to landingpage --- src/components/PractitionerPane.jsx | 10 +- src/components/ProfilePopper.jsx | 4 +- src/pages/LandingPage.jsx | 177 +++++++++++++++++++--------- src/util/api.js | 41 ++++++- 4 files changed, 167 insertions(+), 65 deletions(-) diff --git a/src/components/PractitionerPane.jsx b/src/components/PractitionerPane.jsx index 8504be2..ea1a81b 100644 --- a/src/components/PractitionerPane.jsx +++ b/src/components/PractitionerPane.jsx @@ -83,7 +83,7 @@ function PractitionerHeader({ strTrained, practitioner, poppedPractitioner, setP }; return ( - @@ -150,7 +150,7 @@ function PractitionerHeader({ strTrained, practitioner, poppedPractitioner, setP export default function PractitionerPane({ community, practitioner, poppedPractitioner, setPoppedPractitioner }) { // Determine if we're on SelfServicePage by checking if community.name is "Self Service" - const isSelfService = community.name === 'My Community'; + const isSelfService = community.name === 'My Community' || community.name.includes(','); const sections = [ [community.state, practitioner.state], @@ -177,7 +177,7 @@ export default function PractitionerPane({ community, practitioner, poppedPracti > - - + > {/* content */} { + return Object.values(filters).every((arr) => arr.length === 0); + }; + + // Get community name based on selection state + const getCommunityName = () => { + if (areFiltersEmpty()) { + return 'My Community'; + } + return selectedLocation ? `${selectedLocation.city}, ${selectedLocation.state}` : 'My Community'; + }; + + const community = { + name: getCommunityName(), + state: filters.state || (selectedState ? [selectedState] : []), // Use filters.state if available + activities: filters.activities, + sectors: filters.sectors, + hazards: filters.hazards, + size: filters.size, + totalCategories: + (filters.state?.length || (selectedState ? 1 : 0)) + + filters.activities.length + + filters.sectors.length + + filters.hazards.length + + filters.size.length, + }; + const visiblePractitioners = practitioners.slice(0, displayCount); const hasMorePractitioners = practitioners.length > displayCount; const hasAnyFilters = Object.values(filters).some((arr) => arr.length > 0) || selectedState; @@ -310,25 +337,48 @@ export default function LandingPage() { }, []); useEffect(() => { - if (selectedState || Object.values(filters).some((arr) => arr.length > 0)) { - fetchFilteredPractitioners( - { - state: selectedState ? [selectedState] : [], - ...filters, - }, - setPractitioners - ); + if (Object.values(filters).some((arr) => arr.length > 0)) { + fetchFilteredPractitioners(filters, setPractitioners); } else { setPractitioners([]); } - }, [selectedState, filters]); + }, [filters]); const handleLocationSelect = (event, newValue) => { setSelectedLocation(newValue); if (newValue) { setSelectedState(newValue.state); + setFilters((prev) => ({ + ...prev, + state: [newValue.state], + })); } else { setSelectedState(''); + setFilters((prev) => ({ + ...prev, + state: [], + })); + } + }; + + const handleSelectionChange = (category, newSelections) => { + if (category === 'state') { + // Clear location selection if state is removed + if (newSelections.length === 0) { + setSelectedLocation(null); + setSelectedState(''); + } + // Update filters + setFilters((prev) => ({ + ...prev, + [category]: newSelections, + })); + } else { + // Handle other categories normally + setFilters((prev) => ({ + ...prev, + [category]: newSelections, + })); } }; @@ -520,55 +570,70 @@ export default function LandingPage() { onViewChange={handleViewChange} /> - - {visiblePractitioners.length} out of {practitioners.length} practitioners selected from the{' '} - {totalPractitioners} available in the practitioner registry - + {currentView === 'cards' ? ( + <> + + {visiblePractitioners.length} out of {practitioners.length} practitioners selected from the{' '} + {totalPractitioners} available in the practitioner registry + - - {visiblePractitioners.map((practitioner, index) => ( - + {visiblePractitioners.map((practitioner, index) => ( + + + + ))} - ))} - - - {hasMorePractitioners && ( - - - + + {hasMorePractitioners && ( + + + + )} + + ) : ( + // Comparison view + )} )} diff --git a/src/util/api.js b/src/util/api.js index a8b1778..bd63ab6 100644 --- a/src/util/api.js +++ b/src/util/api.js @@ -122,7 +122,46 @@ export const fetchFilteredPractitioners = (filters, setPractitioners) => { } return matches; - }); + }) + // Calculate match score + .map((rec) => { + let matchCount = 0; + + if (filters.state?.length) { + filters.state.forEach((state) => { + if (rec.state.includes(state)) matchCount++; + }); + } + + if (filters.activities?.length) { + filters.activities.forEach((activity) => { + if (rec.activities.includes(activity)) matchCount++; + }); + } + + if (filters.sectors?.length) { + filters.sectors.forEach((sector) => { + if (rec.sectors.includes(sector)) matchCount++; + }); + } + + if (filters.hazards?.length) { + filters.hazards.forEach((hazard) => { + if (rec.hazards.includes(hazard)) matchCount++; + }); + } + + if (filters.size?.length) { + filters.size.forEach((size) => { + if (rec.size.includes(size)) matchCount++; + }); + } + + rec.matchScore = matchCount; + return rec; + }) + // Sort by match score (highest first) + .sort((a, b) => b.matchScore - a.matchScore); setPractitioners(recs); }, From 06cff294256bf1a1f71e26f7e531737472435b81 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Fri, 6 Dec 2024 13:34:29 -0500 Subject: [PATCH 04/45] practitioner profile opens in new tab --- src/components/PractitionerCard.jsx | 2 ++ src/components/ProfilePopper.jsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/components/PractitionerCard.jsx b/src/components/PractitionerCard.jsx index 12f556e..209c8e5 100644 --- a/src/components/PractitionerCard.jsx +++ b/src/components/PractitionerCard.jsx @@ -103,6 +103,8 @@ export default function PractitionerCard({ practitioner }) { fullWidth variant="contained" href={`#/practitioner/${practitioner.airtableRecId}`} + target="_blank" + rel="noopener noreferrer" startIcon={} sx={{ backgroundColor: theme.palette.primary.midBlue, diff --git a/src/components/ProfilePopper.jsx b/src/components/ProfilePopper.jsx index 8c63c1c..0850177 100644 --- a/src/components/ProfilePopper.jsx +++ b/src/components/ProfilePopper.jsx @@ -119,6 +119,8 @@ export default function ProfilePopper({ practitioner, poppedPractitioner, setPop {/* link to full profile */} Date: Mon, 9 Dec 2024 08:45:38 -0600 Subject: [PATCH 05/45] Titles truncate after two lines --- src/components/PractitionerPane.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/PractitionerPane.jsx b/src/components/PractitionerPane.jsx index ea1a81b..246d13f 100644 --- a/src/components/PractitionerPane.jsx +++ b/src/components/PractitionerPane.jsx @@ -126,13 +126,14 @@ function PractitionerHeader({ strTrained, practitioner, poppedPractitioner, setP sx={{ display: { xs: 'none', - md: 'inherit', + md: '-webkit-box', }, overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', + paddingRight: '10px', //chrome bug where full ellipses won't show without padding textWrap: 'auto', textAlign: 'center', + WebkitLineClamp: '2', + WebkitBoxOrient: 'vertical', }} > {practitioner.org} From 7cee36ea6f6a0b73ab0ddf15a1a931670ff624fc Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Mon, 9 Dec 2024 10:30:42 -0500 Subject: [PATCH 06/45] agol geocoding --- README.md | 2 + src/pages/LandingPage.jsx | 179 ++++++++++++++++++++++---------------- src/util/geocoding.js | 157 +++++++++++++++++++++++++++++++++ vite.config.js | 1 + 4 files changed, 265 insertions(+), 74 deletions(-) create mode 100644 src/util/geocoding.js diff --git a/README.md b/README.md index ee5b245..b1417f2 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,13 @@ ### Local Setup - Create an [Airtable Personal Access Token](https://support.airtable.com/docs/creating-personal-access-tokens) +- Create an [ArcGIS Online API Key](https://developers.arcgis.com/documentation/security-and-authentication/api-key-authentication/tutorials/create-an-api-key/) - Create a `.env.` file in base folder that looks like this: ``` AIRTABLE_TOKEN=YOUR_TOKEN AIRTABLE_BASE=app54Ce7cZjqk6dLw +AGOL_API_KEY=YOUR_API_KEY ``` Run `npm run dev` for local server diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 016e073..c3110ee 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Autocomplete, TextField, + CircularProgress, Typography, Container, Box, @@ -21,62 +22,111 @@ import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; import { fetchFilteredPractitioners, fetchOptionsFromAirtable, fetchAllPractitioners } from '../util/api'; import ComparisonBoard from '../components/ComparisonBoard'; import PractitionerCard from '../components/PractitionerCard'; +import { searchLocations, getLocationDetails } from '../util/geocoding'; 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 LocationSearch = ({ value, onChange, disabled }) => { + const [open, setOpen] = useState(false); + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + const [inputValue, setInputValue] = useState(''); + + // Update input value when value prop changes + useEffect(() => { + if (value?.fullText) { + setInputValue(value.fullText); + } + }, [value]); + + const handleInputChange = async (event, newInputValue) => { + setInputValue(newInputValue); + + if (newInputValue.length >= 3) { + setLoading(true); + const suggestions = await searchLocations(newInputValue); + // Transform suggestions to match the selected value format + const transformedSuggestions = suggestions.map((suggestion) => ({ + ...suggestion, + fullText: suggestion.text, + })); + setOptions(transformedSuggestions); + setLoading(false); + } else { + setOptions([]); + } + }; + + const handleChange = async (event, newValue) => { + if (newValue?.magicKey) { + setLoading(true); + const details = await getLocationDetails(newValue.magicKey); + if (details) { + onChange(event, details); + } + setLoading(false); + } else { + onChange(event, null); + } + }; + + return ( + { + if (!option) return ''; + return option.fullText || option.text || ''; + }} + isOptionEqualToValue={(option, value) => { + if (!option || !value) return false; + return option.fullText === value.fullText; + }} + filterOptions={(x) => x} + autoComplete + includeInputInList + filterSelectedOptions + loading={loading} + loadingText="Searching..." + noOptionsText={inputValue.length < 3 ? 'Type at least 3 characters' : 'No locations found'} + open={open && inputValue.length >= 3} + onOpen={() => setOpen(true)} + onClose={() => setOpen(false)} + disabled={disabled} + sx={{ flexGrow: 1 }} + popupIcon={null} + renderInput={(params) => ( + , + endAdornment: loading ? ( + + ) : null, + }} + /> + )} + /> + ); +}; const FilterSection = ({ title, description, type, selected, availableOptions, onAdd, onRemove }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -448,29 +498,10 @@ export default function LandingPage() { Where is your community? - `${option.city}, ${option.state}`} - sx={{ flexGrow: 1 }} - renderInput={(params) => ( - , - }} - /> - )} + disabled={false} /> {selectedLocation && ( diff --git a/src/util/geocoding.js b/src/util/geocoding.js new file mode 100644 index 0000000..b8080b9 --- /dev/null +++ b/src/util/geocoding.js @@ -0,0 +1,157 @@ +// util/geocoding.js +const GEOCODING_URL = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest'; +const GEOCODING_DETAIL_URL = + 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates'; +const API_KEY = __AGOL_API_KEY__; + +// Map of state/territory abbreviations (both 2 and 3 letter) to full names +export const STATE_MAPPINGS = { + // Standard 2-letter state codes + AL: 'Alabama', + AK: 'Alaska', + AZ: 'Arizona', + AR: 'Arkansas', + CA: 'California', + CO: 'Colorado', + CT: 'Connecticut', + DE: 'Delaware', + DC: 'District of Columbia', + FL: 'Florida', + GA: 'Georgia', + HI: 'Hawaii', + ID: 'Idaho', + IL: 'Illinois', + IN: 'Indiana', + IA: 'Iowa', + KS: 'Kansas', + KY: 'Kentucky', + LA: 'Louisiana', + ME: 'Maine', + MD: 'Maryland', + MA: 'Massachusetts', + MI: 'Michigan', + MN: 'Minnesota', + MS: 'Mississippi', + MO: 'Missouri', + MT: 'Montana', + NE: 'Nebraska', + NV: 'Nevada', + NH: 'New Hampshire', + NJ: 'New Jersey', + NM: 'New Mexico', + NY: 'New York', + NC: 'North Carolina', + ND: 'North Dakota', + OH: 'Ohio', + OK: 'Oklahoma', + OR: 'Oregon', + PA: 'Pennsylvania', + RI: 'Rhode Island', + SC: 'South Carolina', + SD: 'South Dakota', + TN: 'Tennessee', + TX: 'Texas', + UT: 'Utah', + VT: 'Vermont', + VA: 'Virginia', + WA: 'Washington', + WV: 'West Virginia', + WI: 'Wisconsin', + WY: 'Wyoming', + + // 3-letter territory codes + ASM: 'American Samoa', + GUM: 'Guam', + MNP: 'Northern Mariana Islands', + PRI: 'Puerto Rico', + VIR: 'Virgin Islands', + + // Map 2-letter codes to 3-letter codes for territories + AS: 'ASM', + GU: 'GUM', + MP: 'MNP', + PR: 'PRI', + VI: 'VIR', +}; + +// Get the correct state code (2 or 3 letter) for display +export const getDisplayStateCode = (abbr) => { + // If it's a territory with a 2-letter code, convert to 3-letter + if (['AS', 'GU', 'MP', 'PR', 'VI'].includes(abbr)) { + return STATE_MAPPINGS[abbr]; + } + return abbr; +}; + +// Convert state abbreviation to full name +export const getFullStateName = (abbr) => { + if (!abbr) return ''; + + // If it's a 2-letter territory code, get the 3-letter code first + const stateCode = ['AS', 'GU', 'MP', 'PR', 'VI'].includes(abbr) ? STATE_MAPPINGS[abbr] : abbr; + + return STATE_MAPPINGS[stateCode] || abbr; +}; + +export const searchLocations = async (searchText) => { + if (!searchText || searchText.length < 3) return []; + + try { + const params = new URLSearchParams({ + f: 'json', + token: API_KEY, + sourceCountry: 'USA', + category: 'City', + maxSuggestions: 5, + text: searchText, + }); + + const response = await fetch(`${GEOCODING_URL}?${params}`); + const data = await response.json(); + + if (data.suggestions) { + return data.suggestions.map((suggestion) => ({ + text: suggestion.text, + magicKey: suggestion.magicKey, + })); + } + + return []; + } catch (error) { + console.error('Error searching locations:', error); + return []; + } +}; + +export const getLocationDetails = async (magicKey) => { + try { + const params = new URLSearchParams({ + f: 'json', + token: API_KEY, + magicKey: magicKey, + outFields: 'City,RegionAbbr,Region', + }); + + const response = await fetch(`${GEOCODING_DETAIL_URL}?${params}`); + const data = await response.json(); + + if (data.candidates && data.candidates.length > 0) { + const location = data.candidates[0]; + const attributes = location.attributes; + const stateAbbr = attributes.RegionAbbr || attributes.Region; + const displayStateCode = getDisplayStateCode(stateAbbr); + const fullStateName = getFullStateName(stateAbbr); + + return { + city: attributes.City, + state: fullStateName, + fullText: `${attributes.City}, ${displayStateCode}`, + }; + } + + return null; + } catch (error) { + console.error('Error getting location details:', error); + return null; + } +}; diff --git a/vite.config.js b/vite.config.js index 3c95f18..9235243 100644 --- a/vite.config.js +++ b/vite.config.js @@ -8,6 +8,7 @@ export default defineConfig(({ command, mode }) => { define: { __AIRTABLE_TOKEN__: JSON.stringify(env.AIRTABLE_TOKEN), __AIRTABLE_BASE__: JSON.stringify(env.AIRTABLE_BASE), + __AGOL_API_KEY__: JSON.stringify(env.AGOL_API_KEY), }, //base: 'https://nemac.github.io/crf-matching', plugins: [react()], From 130da9678d238667ab836d1896e8838d7efeab7e Mon Sep 17 00:00:00 2001 From: Dani Levy Date: Mon, 9 Dec 2024 10:00:23 -0600 Subject: [PATCH 07/45] Mobile styling for filtering --- src/pages/LandingPage.jsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 016e073..6965b49 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -116,8 +116,16 @@ const FilterSection = ({ title, description, type, selected, availableOptions, o @@ -132,6 +140,8 @@ const FilterSection = ({ title, description, type, selected, availableOptions, o sx={{ textTransform: 'none', color: 'primary.main', + textDecoration: 'underline', + padding: '6px 0', }} > Learn more @@ -433,9 +443,16 @@ export default function LandingPage() { Date: Mon, 9 Dec 2024 11:03:00 -0500 Subject: [PATCH 08/45] better position for view more button --- src/components/ComparisonBoard.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ComparisonBoard.jsx b/src/components/ComparisonBoard.jsx index 1dd029c..273a3e4 100644 --- a/src/components/ComparisonBoard.jsx +++ b/src/components/ComparisonBoard.jsx @@ -57,7 +57,8 @@ export default function ComparisonBoard({ {/* Practitioners Panel */} - + {/* Header Area with View More Button */} + + + + { + const params = new URLSearchParams(); + + // Handle location + if (selectedLocation) { + params.set('city', selectedLocation.city); + params.set('state', selectedLocation.state); + } + + // Handle filter arrays + Object.entries(filters).forEach(([key, values]) => { + if (values && values.length > 0) { + // Use comma-separated values for arrays + params.set(key, values.join(',')); + } + }); + + return params; +}; + +// Parse URL search params back to filters object +export const searchParamsToFilters = async (searchParams) => { + const filters = { + activities: [], + sectors: [], + hazards: [], + size: [], + state: [], + }; + + const location = { + selectedLocation: null, + selectedState: '', + }; + + // Parse each filter type + Object.keys(filters).forEach((key) => { + const param = searchParams.get(key); + if (param) { + filters[key] = param.split(','); + } + }); + + // Handle location separately + const city = searchParams.get('city'); + const state = searchParams.get('state'); + + if (city && state) { + // Reconstruct location object + const locationDetails = { + city, + state, + fullText: `${city}, ${state}`, + }; + + location.selectedLocation = locationDetails; + location.selectedState = state; + + // Ensure state is in filters + if (!filters.state.includes(state)) { + filters.state = [state]; + } + } + + return { filters, location }; +}; + +// Generate shareable URL +export const generateShareableUrl = (filters, selectedLocation) => { + const params = filtersToSearchParams(filters, selectedLocation); + const baseUrl = window.location.origin + window.location.pathname; + return `${baseUrl}#/?${params.toString()}`; +}; From 5e405b216dcbe244988f4032d634b3e5850bb0c8 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Tue, 10 Dec 2024 10:45:58 -0500 Subject: [PATCH 15/45] practitioner registry links to climateresiliencefund --- src/pages/LandingPage.jsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 7af4f9f..cfb4fe6 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -582,7 +582,7 @@ export default function LandingPage() { {visiblePractitioners.length} out of {practitioners.length} practitioners selected from the{' '} - {totalPractitioners} available in the practitioner registry + {totalPractitioners} available in the{' '} + + practitioner registry + Date: Tue, 10 Dec 2024 10:56:53 -0500 Subject: [PATCH 16/45] changed hashrouter to browserrouter --- src/components/PractitionerCard.jsx | 2 +- src/components/PractitionerPane.jsx | 2 +- src/components/ProfilePopper.jsx | 2 +- src/main.jsx | 4 ++-- src/pages/CommunityListPage.jsx | 2 +- src/pages/OldLandingPage.jsx | 6 +++--- src/util/urlStateManagement.js | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/PractitionerCard.jsx b/src/components/PractitionerCard.jsx index a784885..00ed90d 100644 --- a/src/components/PractitionerCard.jsx +++ b/src/components/PractitionerCard.jsx @@ -101,7 +101,7 @@ export default function PractitionerCard({ practitioner, onComparisonSelect, isS + )} {/* Filter Sections */} @@ -762,7 +806,7 @@ export default function LandingPage() { {visiblePractitioners.length} out of {practitioners.length} practitioners selected from the{' '} {totalPractitioners} available in the{' '} Date: Wed, 11 Dec 2024 10:06:08 -0500 Subject: [PATCH 21/45] sorted size --- src/util/api.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/util/api.js b/src/util/api.js index bd63ab6..1af68ab 100644 --- a/src/util/api.js +++ b/src/util/api.js @@ -362,14 +362,30 @@ export const fetchOptionsFromAirtable = (setOptions) => { } }); + // Define custom sort order for size + const sizeOrder = [ + 'Under 10k', + '10k-50k', + '50k-100k', + '100k-200k', + '200k-300k', + '300k-400k', + '400k-500k', + 'Over 500k', + ]; + // Convert Sets to sorted arrays const options = { state: [...availableOptions.state].sort(), activities: [...availableOptions.activities].sort(), hazards: [...availableOptions.hazards].sort(), - size: [...availableOptions.size].sort(), + // Custom sort for size based on defined order + size: [...availableOptions.size].sort((a, b) => { + return sizeOrder.indexOf(a) - sizeOrder.indexOf(b); + }), sectors: [...availableOptions.sectors].sort(), }; + setOptions(options); }); }; From df9c95cb245ca0f748a7f76b955906d97c51f1b3 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Wed, 11 Dec 2024 10:18:26 -0500 Subject: [PATCH 22/45] add clear selected button --- src/pages/LandingPage.jsx | 78 ++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 5a3bdd9..618f50c 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -23,6 +23,7 @@ import WindowIcon from '@mui/icons-material/Window'; import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; import ShareIcon from '@mui/icons-material/Share'; import ClearAllIcon from '@mui/icons-material/ClearAll'; +import { PersonOffOutlined } from '@mui/icons-material'; import { fetchFilteredPractitioners, fetchOptionsFromAirtable, fetchAllPractitioners } from '../util/api'; import Toast from '../components/Toast'; import ComparisonBoard from '../components/ComparisonBoard'; @@ -281,7 +282,7 @@ const FilterSection = ({ title, description, type, selected, availableOptions, o ); }; -const ViewToggle = ({ view, onViewChange }) => { +const ViewToggle = ({ view, onViewChange, selectedCount, onClearSelected }) => { return ( { width: '100%', mb: 3, gap: 2, + position: 'relative', // For absolute positioning of clear button }} > { Compare + + {/* Clear Selected Button - Only show when there are selected practitioners */} + {selectedCount > 0 && ( + + )} ); }; @@ -521,9 +547,16 @@ export default function LandingPage() { const handleViewChange = (event, newView) => { if (newView !== null) { + // When switching to compare view, filter practitioners to only show selected ones if (newView === 'compare' && selectedForComparison.size > 0) { - // Reset display count when switching to compare view with selections - setDisplayCount(selectedForComparison.size); + // Create a filtered version of practitioners that only includes selected ones + const selectedPractitioners = practitioners.filter((p) => selectedForComparison.has(p.airtableRecId)); + // Update display count to match number of selected practitioners + setDisplayCount(selectedPractitioners.length); + } else if (newView === 'cards') { + // When switching back to cards view, reset to show all practitioners + // but maintain original pagination + setDisplayCount(PRACTITIONERS_PER_PAGE); } setCurrentView(newView); } @@ -569,6 +602,10 @@ export default function LandingPage() { }); }; + const handleClearSelectedPractitioners = () => { + setSelectedForComparison(new Set()); + }; + return ( - - {currentView === 'cards' ? ( <> ))} - - {hasMorePractitioners && ( - - - - )} ) : ( - // Comparison view + // Compare view - // If there are any selected practitioners, only show those - // Otherwise show all practitioners selectedForComparison.size === 0 ? true : selectedForComparison.has(p.airtableRecId) )} isSelectable={true} @@ -880,7 +890,7 @@ export default function LandingPage() { displayCount={displayCount} setDisplayCount={setDisplayCount} /> - )} + )}{' '} )} From 8f45100829f30935820b959e0288423e3ff2c835 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Wed, 11 Dec 2024 10:23:27 -0500 Subject: [PATCH 23/45] accidentally removed load more practitioners button --- src/pages/LandingPage.jsx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 618f50c..66a33f2 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -876,6 +876,31 @@ export default function LandingPage() { ))} + {/* Load More Button */} + {hasMorePractitioners && ( + + + + )} ) : ( // Compare view From 3829de907eddf692241390e3597dd0bcdf57000c Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Wed, 11 Dec 2024 10:54:17 -0500 Subject: [PATCH 24/45] make str badge look less like a button --- src/components/PractitionerPane.jsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/PractitionerPane.jsx b/src/components/PractitionerPane.jsx index 6c5876d..92e671a 100644 --- a/src/components/PractitionerPane.jsx +++ b/src/components/PractitionerPane.jsx @@ -23,38 +23,43 @@ function StrTrainedBadge({ isTrained }) { if (isTrained === 'Yes') { return ( - + STR Trained From 44ed01133a23d535b0e7dc1939155d99788b9665 Mon Sep 17 00:00:00 2001 From: Dani Levy Date: Wed, 11 Dec 2024 10:41:48 -0600 Subject: [PATCH 25/45] Logo component and added to pages --- src/assets/CSCI_logo.png | Bin 0 -> 9302 bytes src/components/ComparisonBoard.jsx | 2 ++ src/components/Logo.jsx | 25 +++++++++++++++++++++++++ src/pages/CommunityListPage.jsx | 2 ++ src/pages/LandingPage.jsx | 2 ++ src/pages/PractitionerListPage.jsx | 2 ++ src/pages/PractitionerPage.jsx | 6 ++++-- 7 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/assets/CSCI_logo.png create mode 100644 src/components/Logo.jsx diff --git a/src/assets/CSCI_logo.png b/src/assets/CSCI_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..883aeb53bc42969380854a3139c101f57b5afef9 GIT binary patch literal 9302 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 + {/* CSCI Logo */} + CSCI Logo + + ) +} \ No newline at end of file diff --git a/src/pages/CommunityListPage.jsx b/src/pages/CommunityListPage.jsx index fe26b9b..63daecf 100644 --- a/src/pages/CommunityListPage.jsx +++ b/src/pages/CommunityListPage.jsx @@ -15,6 +15,7 @@ import { fetchAllCommunities } from '../util/api'; // components import FullPageSpinner from '../components/FullPageSpinner'; +import Logo from '../components/Logo'; function CommunitiesPageLoaded({ communities }) { return ( @@ -25,6 +26,7 @@ function CommunitiesPageLoaded({ communities }) { }} >
+ {/* CSCI Logo */}

CRF Community Matching Tool

diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 121c7f1..9702bc3 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -27,6 +27,7 @@ import ComparisonBoard from '../components/ComparisonBoard'; import PractitionerCard from '../components/PractitionerCard'; import { searchLocations, getLocationDetails } from '../util/geocoding'; import { filtersToSearchParams, searchParamsToFilters, generateShareableUrl } from '../util/urlStateManagement'; +import Logo from '../components/Logo'; const PRACTITIONERS_PER_PAGE = 6; @@ -556,6 +557,7 @@ export default function LandingPage() { maxWidth="lg" sx={{ mt: 4 }} > + {/* CSCI Logo */} + {/* CSCI Logo */} - + + {/* CSCI Logo */} + {/* Header */} From da4108cc40c9f7a952e372b6f089bdb274e45421 Mon Sep 17 00:00:00 2001 From: Dani Levy Date: Wed, 11 Dec 2024 11:05:50 -0600 Subject: [PATCH 26/45] Fixed logo displaying twice on landing page --- src/components/ComparisonBoard.jsx | 2 -- src/pages/CommunityListPage.jsx | 2 +- src/pages/CommunityPage.jsx | 21 ++++++++++++++------- src/pages/SelfServicePage.jsx | 25 ++++++++++++++++--------- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/components/ComparisonBoard.jsx b/src/components/ComparisonBoard.jsx index 63fa02d..0ed019a 100644 --- a/src/components/ComparisonBoard.jsx +++ b/src/components/ComparisonBoard.jsx @@ -5,7 +5,6 @@ import { ThemeProvider } from '@mui/material/styles'; import PractitionerPane from './PractitionerPane'; import CommunityPane from './CommunityPane'; -import Logo from './Logo'; import theme from '../theme'; import { RowHoverContext, SetHoverRowContext } from './RowHoverContext'; @@ -38,7 +37,6 @@ export default function ComparisonBoard({ maxWidth="xl" sx={{ p: 2 }} > - {/* CSCI Logo */}
diff --git a/src/pages/CommunityPage.jsx b/src/pages/CommunityPage.jsx index e405666..1ec6d2c 100644 --- a/src/pages/CommunityPage.jsx +++ b/src/pages/CommunityPage.jsx @@ -1,7 +1,9 @@ import { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { fetchCommunity, fetchPractitionersForCommunity } from '../util/api'; +import { Container } from '@mui/material'; import ComparisonBoard from '../components/ComparisonBoard'; +import Logo from '../components/Logo'; import FullPageSpinner from '../components/FullPageSpinner'; export default function CommunityPage() { @@ -20,12 +22,17 @@ export default function CommunityPage() { } return ( - + <> + + {/* CSCI Logo */} + + + ); } diff --git a/src/pages/SelfServicePage.jsx b/src/pages/SelfServicePage.jsx index 5a89e08..53660a1 100644 --- a/src/pages/SelfServicePage.jsx +++ b/src/pages/SelfServicePage.jsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react'; import { fetchPractitionersByFilters, fetchOptionsFromAirtable } from '../util/api'; +import { Container } from '@mui/material'; import ComparisonBoard from '../components/ComparisonBoard'; +import Logo from '../components/Logo'; export default function SelfServicePage() { const [selectedOptions, setSelectedOptions] = useState({ @@ -67,14 +69,19 @@ export default function SelfServicePage() { } return ( - + <> + + {/* CSCI Logo */} + + + ); } From 8bb78a090528cf8ab927b1fbd157bd944192271b Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Wed, 11 Dec 2024 13:07:17 -0500 Subject: [PATCH 27/45] add info button --- src/components/CommunityPane.jsx | 1 - src/components/HeaderBox.jsx | 9 +- src/components/PractitionerPane.jsx | 157 ++++++++++++------ src/components/ProfilePopper.jsx | 240 +++++++++++++--------------- 4 files changed, 219 insertions(+), 188 deletions(-) diff --git a/src/components/CommunityPane.jsx b/src/components/CommunityPane.jsx index 0096f41..65b99d6 100644 --- a/src/components/CommunityPane.jsx +++ b/src/components/CommunityPane.jsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { Typography, Box, Stack } from '@mui/material'; import HeaderBox from './HeaderBox'; import ScoreSection from './ScoreSection'; diff --git a/src/components/HeaderBox.jsx b/src/components/HeaderBox.jsx index 1f3c78d..0b50487 100644 --- a/src/components/HeaderBox.jsx +++ b/src/components/HeaderBox.jsx @@ -1,12 +1,9 @@ - -import { Box } from "@mui/material"; -import { styled } from "@mui/material/styles"; - +import { Box } from '@mui/material'; +import { styled } from '@mui/material/styles'; const HeaderBox = styled(Box)(({ theme }) => ({ - height: '225px', + height: '275px', alignContent: 'center', })); export default HeaderBox; - diff --git a/src/components/PractitionerPane.jsx b/src/components/PractitionerPane.jsx index 92e671a..1f8e701 100644 --- a/src/components/PractitionerPane.jsx +++ b/src/components/PractitionerPane.jsx @@ -1,7 +1,9 @@ import { useRef } from 'react'; -import { Typography, Box, styled } from '@mui/material'; +import { Typography, Box, styled, IconButton } from '@mui/material'; import PersonIcon from '@mui/icons-material/Person'; import SchoolIcon from '@mui/icons-material/School'; +import Button from '@mui/material/Button'; +import InfoIcon from '@mui/icons-material/Info'; import ProfilePopper from './ProfilePopper'; import HeaderBox from './HeaderBox'; @@ -26,31 +28,18 @@ function StrTrainedBadge({ isTrained }) { sx={{ display: 'inline-flex', width: '100%', - borderRadius: { - xs: 0, - md: 2, - }, bgcolor: { xs: 'primary.lightBlue', md: 'transparent', }, - color: { - xs: 'primary.main', - md: 'primary.main', - }, - border: { - xs: 'none', - md: `1px solid ${theme.palette.primary.main}`, - }, + color: theme.palette.primary.main, flexGrow: 'space-around', verticalAlign: 'middle', justifyContent: 'center', alignItems: 'center', - px: 2, - py: 1, }} > - + - STR Trained + StR Trained ); @@ -82,18 +71,27 @@ function StrTrainedBadge({ isTrained }) { function PractitionerHeader({ strTrained, practitioner, poppedPractitioner, setPoppedPractitioner }) { const headerRef = useRef(null); + const timeoutRef = useRef(null); - const onMouseEnter = (e) => { + const handleMouseEnter = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } setPoppedPractitioner(practitioner); }; + const handleMouseLeave = () => { + timeoutRef.current = setTimeout(() => { + setPoppedPractitioner(null); + }, 100); + }; + return ( - {/* practitioner label - hidden on xs */} - - {practitioner.org} - - - + + {practitioner.org} + + + + + + + {/* Bottom container for badge, popper, and button */} + + + + + + + + ); } @@ -178,13 +236,11 @@ export default function PractitionerPane({ community, practitioner, poppedPracti return ( (
- {/* Add invisible spacer that matches "Add another" button if on SelfServicePage */} {isSelfService && ( - {/* Empty box with same dimensions as Add Another button */} - + /> )}
))} diff --git a/src/components/ProfilePopper.jsx b/src/components/ProfilePopper.jsx index 86d900e..89e8b79 100644 --- a/src/components/ProfilePopper.jsx +++ b/src/components/ProfilePopper.jsx @@ -2,154 +2,136 @@ import Popper from '@mui/material/Popper'; import ContactRow from './ContactRow'; import CloseIcon from '@mui/icons-material/Close'; import PersonIcon from '@mui/icons-material/Person'; -import { IconButton, Typography, Box, Stack, Button, ClickAwayListener } from '@mui/material'; +import { IconButton, Typography, Box, Stack, Button } from '@mui/material'; import theme from '../theme'; -export default function ProfilePopper({ practitioner, poppedPractitioner, setPoppedPractitioner, headerRef }) { +export default function ProfilePopper({ + practitioner, + poppedPractitioner, + setPoppedPractitioner, + headerRef, + onMouseEnter, + onMouseLeave, +}) { const open = practitioner === poppedPractitioner; const id = open ? `profile-popper-${practitioner.id}` : undefined; - const handleClose = (e) => { - setPoppedPractitioner(null); - }; - return ( - - + - {/* triangle above box */} - + > + {practitioner.org} + - {/* content */} - - {/* close button */} - - - - - {/* title and description */} - {practitioner.org} + Practitioner Org Contact + + + + + - {/* inner box */} - - - Practitioner Org Contact - - - - - - - - {/* link to full profile */} - - - - - - + {/* link to full profile */} + {/**/} + {/* }*/} + {/* >*/} + {/* Full Practitioner Org Profile*/} + {/* */} + {/**/} +
+ ); } From 4ba597c635007eda21fd64a2230197ac47523a5c Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 09:30:29 -0500 Subject: [PATCH 28/45] Added a browse all button --- src/pages/LandingPage.jsx | 76 +++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 1ec620b..85441ac 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -24,6 +24,7 @@ import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; import ShareIcon from '@mui/icons-material/Share'; import ClearAllIcon from '@mui/icons-material/ClearAll'; import { PersonOffOutlined } from '@mui/icons-material'; +import { FormatListBulleted } from '@mui/icons-material'; import { fetchFilteredPractitioners, fetchOptionsFromAirtable, fetchAllPractitioners } from '../util/api'; import Toast from '../components/Toast'; import ComparisonBoard from '../components/ComparisonBoard'; @@ -693,7 +694,6 @@ export default function LandingPage() { alignItems: 'center', justifyContent: 'space-between', gap: 1, - cursor: 'pointer', }} > setShowFilters(!showFilters)} > @@ -713,28 +714,57 @@ export default function LandingPage() { Filter practitioners by their expertise - {/* Only show clear button if there are filters applied */} - {(filters.activities.length > 0 || - filters.sectors.length > 0 || - filters.hazards.length > 0 || - filters.size.length > 0) && ( - - )} + + {/* Only show browse all when no community is selected */} + {!selectedLocation && ( + + )} + + {/* Only show clear button if there are filters applied */} + {(filters.activities.length > 0 || + filters.sectors.length > 0 || + filters.hazards.length > 0 || + filters.size.length > 0) && ( + + )} + {/* Filter Sections */} From 2cb2e71d89483179e5fb61b21364fd2016496b6c Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 09:36:12 -0500 Subject: [PATCH 29/45] auto expand filters when selecting a community --- src/pages/LandingPage.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 85441ac..bd308c7 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -492,6 +492,7 @@ export default function LandingPage() { ...prev, state: [newValue.state], })); + setShowFilters(true); } else { setSelectedState(''); setFilters((prev) => ({ From 144190dcda48e02b0fcaceb85ca35b809737db1d Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 09:38:16 -0500 Subject: [PATCH 30/45] clear filters also clears community --- src/pages/LandingPage.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index bd308c7..18ee13a 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -538,13 +538,14 @@ export default function LandingPage() { }; const handleClearAllFilters = () => { + setSelectedLocation(null); + setShowFilters(false); setFilters({ activities: [], sectors: [], hazards: [], size: [], - // Don't clear state/location as that's handled separately - state: filters.state, + state: [], }); }; From 22b856c50d0f8714b59a006736ac4f39dfb86493 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 09:50:24 -0500 Subject: [PATCH 31/45] added x circle icon to community --- src/pages/LandingPage.jsx | 40 ++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 18ee13a..4dc0b96 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -25,6 +25,8 @@ import ShareIcon from '@mui/icons-material/Share'; import ClearAllIcon from '@mui/icons-material/ClearAll'; import { PersonOffOutlined } from '@mui/icons-material'; import { FormatListBulleted } from '@mui/icons-material'; +import CancelIcon from '@mui/icons-material/Cancel'; +import IconButton from '@mui/material/IconButton'; import { fetchFilteredPractitioners, fetchOptionsFromAirtable, fetchAllPractitioners } from '../util/api'; import Toast from '../components/Toast'; import ComparisonBoard from '../components/ComparisonBoard'; @@ -79,6 +81,12 @@ const LocationSearch = ({ value, onChange, disabled }) => { } }; + const handleClear = (event) => { + event.stopPropagation(); // Prevent triggering other click handlers + onChange(null, null); + setInputValue(''); + }; + return ( { '& .MuiOutlinedInput-root': { borderRadius: 1, '& .MuiOutlinedInput-input': { - paddingRight: '14px !important', + paddingRight: value ? '64px !important' : '14px !important', }, }, }} InputProps={{ ...params.InputProps, startAdornment: , - endAdornment: loading ? ( - - ) : null, + endAdornment: ( + <> + {loading ? ( + + ) : value ? ( + + + + ) : null} + + ), }} /> )} From d0668f36b93fbacb09c9a4a9ee6471b63c0f134c Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 09:51:54 -0500 Subject: [PATCH 32/45] change Size to Community Population --- src/components/CommunityPane.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CommunityPane.jsx b/src/components/CommunityPane.jsx index 65b99d6..8f3a2c1 100644 --- a/src/components/CommunityPane.jsx +++ b/src/components/CommunityPane.jsx @@ -28,7 +28,7 @@ const getSectionData = (community, isSelectable, availableOptions, onSelectionCh availableSelections: isSelectable ? availableOptions?.hazards || [] : [], }, { - header: 'Size', + header: 'Community Population', cards: community.size, availableSelections: isSelectable ? availableOptions?.size || [] : [], }, From 159f68a3f54c68b5d70b14c3b89633bd73040413 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 10:00:56 -0500 Subject: [PATCH 33/45] slightly darken background when card is selected --- src/components/PractitionerCard.jsx | 250 ++++++++++++++-------------- 1 file changed, 128 insertions(+), 122 deletions(-) diff --git a/src/components/PractitionerCard.jsx b/src/components/PractitionerCard.jsx index 00ed90d..4f360df 100644 --- a/src/components/PractitionerCard.jsx +++ b/src/components/PractitionerCard.jsx @@ -10,137 +10,143 @@ export default function PractitionerCard({ practitioner, onComparisonSelect, isS const displayedActivities = practitioner.activities.slice(0, 3); return ( - - - - Company Logo - - - - {practitioner.org} - - - - {truncatedDescription} - - - - - Adaptation Expertise - + + + - {displayedActivities.map((activity, index) => ( - - {activity} - - ))} + Company Logo - - - + {practitioner.org} + - onComparisonSelect(practitioner.airtableRecId, e.target.checked)} - sx={{ - color: theme.palette.primary.main, - '&.Mui-checked': { + + {truncatedDescription} + + + + + Adaptation Expertise + + + {displayedActivities.map((activity, index) => ( + - } - label="Compare this practitioner" - sx={{ - '& .MuiFormControlLabel-label': { - fontSize: '0.875rem', - color: theme.palette.primary.main, - }, - }} - /> - - - + fontSize: '0.9rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '32px', + textAlign: 'center', + }} + > + {activity} + + ))} + + + + + + + onComparisonSelect(practitioner.airtableRecId, e.target.checked)} + sx={{ + color: theme.palette.primary.main, + '&.Mui-checked': { + color: theme.palette.primary.main, + }, + }} + /> + } + label="Compare this practitioner" + sx={{ + '& .MuiFormControlLabel-label': { + fontSize: '0.875rem', + color: theme.palette.primary.main, + }, + }} + /> + + + + ); } From 5f3c925f3811da14594910425f3bb7a6d5b36adb Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 11:48:00 -0500 Subject: [PATCH 34/45] Updated Browse All button --- src/pages/LandingPage.jsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 4dc0b96..b927c4e 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -746,10 +746,18 @@ export default function LandingPage() { {/* Only show browse all when no community is selected */} {!selectedLocation && ( )} From 1f86f807f528cd548f8b6f348b6998d6f69e8226 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 12:44:52 -0500 Subject: [PATCH 35/45] fix state bug and also change filter button style --- src/pages/LandingPage.jsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index b927c4e..358cc30 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -577,16 +577,11 @@ export default function LandingPage() { const handleViewChange = (event, newView) => { if (newView !== null) { - // When switching to compare view, filter practitioners to only show selected ones + // When switching views, maintain the current displayCount unless we're filtering for selected practitioners if (newView === 'compare' && selectedForComparison.size > 0) { - // Create a filtered version of practitioners that only includes selected ones + // Only show selected practitioners in compare view if there are any selected const selectedPractitioners = practitioners.filter((p) => selectedForComparison.has(p.airtableRecId)); - // Update display count to match number of selected practitioners setDisplayCount(selectedPractitioners.length); - } else if (newView === 'cards') { - // When switching back to cards view, reset to show all practitioners - // but maintain original pagination - setDisplayCount(PRACTITIONERS_PER_PAGE); } setCurrentView(newView); } @@ -737,7 +732,13 @@ export default function LandingPage() { Filter practitioners by their expertise From f5fba7c5e6b0c0debb7f0f5f7f82e350f3472ee5 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 12:51:15 -0500 Subject: [PATCH 36/45] fixed card and compare button changing size when clicked --- src/components/ComparisonBoard.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ComparisonBoard.jsx b/src/components/ComparisonBoard.jsx index 0ed019a..6d67a03 100644 --- a/src/components/ComparisonBoard.jsx +++ b/src/components/ComparisonBoard.jsx @@ -32,7 +32,6 @@ export default function ComparisonBoard({ - Date: Thu, 12 Dec 2024 13:03:19 -0500 Subject: [PATCH 37/45] rudimentary sharable practitioners working. Probably some bugs --- src/pages/LandingPage.jsx | 28 ++++++++++++++++++++++------ src/util/urlStateManagement.js | 28 +++++++++++++--------------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 358cc30..b189dc9 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -423,12 +423,23 @@ export default function LandingPage() { useEffect(() => { const loadStateFromUrl = async () => { if (searchParams.toString()) { - const { filters: urlFilters, location, view } = await searchParamsToFilters(searchParams); + const { + filters: urlFilters, + location, + view, + selectedPractitioners, + } = await searchParamsToFilters(searchParams); // Update all state from URL setFilters(urlFilters); setSelectedLocation(location.selectedLocation); setSelectedState(location.selectedState); + + // Set selected practitioners from URL + if (selectedPractitioners.length > 0) { + setSelectedForComparison(new Set(selectedPractitioners)); + } + if (view) { setCurrentView(view); } @@ -436,18 +447,18 @@ export default function LandingPage() { }; loadStateFromUrl(); - }, []); // Only run on mount + }, []); // Helper to check if all filters are empty const areFiltersEmpty = () => { return Object.values(filters).every((arr) => arr.length === 0); }; - // Update URL when filters or location change + // Update URL when filters, location, or selected practitioners change useEffect(() => { - const params = filtersToSearchParams(filters, selectedLocation, currentView); + const params = filtersToSearchParams(filters, selectedLocation, currentView, Array.from(selectedForComparison)); setSearchParams(params); - }, [filters, selectedLocation, currentView]); + }, [filters, selectedLocation, currentView, selectedForComparison]); useEffect(() => { fetchOptionsFromAirtable(setAvailableOptions); @@ -496,7 +507,12 @@ export default function LandingPage() { const hasAnyFilters = Object.values(filters).some((arr) => arr.length > 0) || selectedState; const handleShare = async () => { - const shareableUrl = generateShareableUrl(filters, selectedLocation, currentView); + const shareableUrl = generateShareableUrl( + filters, + selectedLocation, + currentView, + Array.from(selectedForComparison) + ); try { await navigator.clipboard.writeText(shareableUrl); diff --git a/src/util/urlStateManagement.js b/src/util/urlStateManagement.js index cf1e9ff..b0184b9 100644 --- a/src/util/urlStateManagement.js +++ b/src/util/urlStateManagement.js @@ -1,9 +1,4 @@ -// util/urlStateManagement.js - -import { getLocationDetails } from './geocoding'; - -// Convert filters object to URL search params -export const filtersToSearchParams = (filters, selectedLocation, view) => { +export const filtersToSearchParams = (filters, selectedLocation, view, selectedPractitioners = []) => { const params = new URLSearchParams(); // Handle location @@ -17,10 +12,14 @@ export const filtersToSearchParams = (filters, selectedLocation, view) => { params.set('view', view); } + // Handle selected practitioners + if (selectedPractitioners.length > 0) { + params.set('selected', selectedPractitioners.join(',')); + } + // Handle filter arrays Object.entries(filters).forEach(([key, values]) => { if (values && values.length > 0) { - // Use comma-separated values for arrays params.set(key, values.join(',')); } }); @@ -28,7 +27,6 @@ export const filtersToSearchParams = (filters, selectedLocation, view) => { return params; }; -// Parse URL search params back to filters object export const searchParamsToFilters = async (searchParams) => { const filters = { activities: [], @@ -45,6 +43,10 @@ export const searchParamsToFilters = async (searchParams) => { const view = searchParams.get('view'); + // Parse selected practitioners + const selectedParam = searchParams.get('selected'); + const selectedPractitioners = selectedParam ? selectedParam.split(',') : []; + // Parse each filter type Object.keys(filters).forEach((key) => { const param = searchParams.get(key); @@ -58,7 +60,6 @@ export const searchParamsToFilters = async (searchParams) => { const state = searchParams.get('state'); if (city && state) { - // Reconstruct location object const locationDetails = { city, state, @@ -68,19 +69,16 @@ export const searchParamsToFilters = async (searchParams) => { location.selectedLocation = locationDetails; location.selectedState = state; - // Ensure state is in filters if (!filters.state.includes(state)) { filters.state = [state]; } } - return { filters, location, view }; + return { filters, location, view, selectedPractitioners }; }; -// Generate shareable URL -export const generateShareableUrl = (filters, selectedLocation, view) => { - const params = filtersToSearchParams(filters, selectedLocation, view); - // Remove any trailing slashes from origin and ensure clean path +export const generateShareableUrl = (filters, selectedLocation, view, selectedPractitioners) => { + const params = filtersToSearchParams(filters, selectedLocation, view, selectedPractitioners); const baseUrl = window.location.origin.replace(/\/$/, ''); return `${baseUrl}?${params.toString()}`; }; From 71d40d3d3238c8868e0ee11ea4c37824c40d498c Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 13:11:49 -0500 Subject: [PATCH 38/45] update Practitioner Page per meeting feedback --- src/pages/PractitionerPage.jsx | 48 +++++++++++++++++++++++----------- src/util/api.js | 8 ++++-- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/pages/PractitionerPage.jsx b/src/pages/PractitionerPage.jsx index 2c42eac..8e85458 100644 --- a/src/pages/PractitionerPage.jsx +++ b/src/pages/PractitionerPage.jsx @@ -62,11 +62,10 @@ function StrTrainedRow({ isTrained }) { display: 'inline-flex', alignItems: 'center', gap: 2, - }}> + }} + > - - STR Training Class Completed - + STR Training Class Completed ); } @@ -126,9 +125,11 @@ function PractitionerPageLoaded({ practitioner }) { return ( - + {/* CSCI Logo */} - {/* Header */} {practitioner.org} - {/* Contact & Training Row */} - - - + - {practitioner.exampleStakeholders || 'N/A'} + {practitioner.organizationType || 'N/A'} - - + - {practitioner.exampleMultipleBenefits || 'N/A'} + {practitioner.additionalInformation || 'N/A'} + + + + + + {practitioner.languageFluencies || 'N/A'} + + + + + + {practitioner.specificTypesOfCommunities || 'N/A'} - {sections.map((data, index) => { return ( Date: Thu, 12 Dec 2024 12:15:29 -0600 Subject: [PATCH 39/45] Choose filters is styled as a button --- src/pages/LandingPage.jsx | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 1ec620b..b471aa4 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -690,29 +690,32 @@ export default function LandingPage() { - } + onClick={() => setShowFilters(!showFilters)} sx={{ - display: 'flex', - alignItems: 'center', - gap: 1, - cursor: 'pointer', + textTransform: 'none', + bgcolor: 'grey.500', + '&:hover': { + bgcolor: 'grey.600', + }, }} - onClick={() => setShowFilters(!showFilters)} > - - - Filter practitioners by their expertise - - + Filter practitioners by their expertise + + {/* Only show clear button if there are filters applied */} {(filters.activities.length > 0 || filters.sectors.length > 0 || @@ -790,6 +793,7 @@ export default function LandingPage() { alignItems: 'center', justifyContent: 'space-between', mb: 3, + gap: 1, flexDirection: { xs: 'column', md: 'row', From 4b0e814415ccee276c8ce873058c8527d02369bd Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 13:48:16 -0500 Subject: [PATCH 40/45] commenting this out for now --- src/pages/LandingPage.jsx | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index b189dc9..eca5a2a 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -222,17 +222,17 @@ const FilterSection = ({ title, description, type, selected, availableOptions, o > {title} - + {/**/} + {/* Learn more*/} + {/**/} Date: Thu, 12 Dec 2024 12:49:37 -0600 Subject: [PATCH 41/45] Title cell styling --- src/components/PractitionerPane.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/PractitionerPane.jsx b/src/components/PractitionerPane.jsx index 1f8e701..0eac6e5 100644 --- a/src/components/PractitionerPane.jsx +++ b/src/components/PractitionerPane.jsx @@ -93,7 +93,7 @@ function PractitionerHeader({ strTrained, practitioner, poppedPractitioner, setP justifyContent: 'space-between', alignItems: 'center', margin: '0 auto', - padding: '8px 0', + padding: '8px 1px', // 1px L/R padding to fix Chrome bug where full ellipses don't show display: { md: 'flex', xs: 'block', @@ -130,7 +130,8 @@ function PractitionerHeader({ strTrained, practitioner, poppedPractitioner, setP display: 'flex', alignItems: 'center', justifyContent: 'center', - mb: 1, // Add margin bottom to reduce space between title and STR trained + margin: '8px 0', // Add margin to increase space between icon and STR trained + flexDirection: 'column', // Stack title and icon }} > - View Full Profile + View Full Profile + View Profile From 88787309a1f01e9fb058680d76eb1749084acf28 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 12 Dec 2024 14:44:47 -0500 Subject: [PATCH 42/45] fixing issues that occurred during merge conflict --- src/components/CommunityPane.jsx | 2 +- src/pages/LandingPage.jsx | 23 ----------------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/components/CommunityPane.jsx b/src/components/CommunityPane.jsx index 8f3a2c1..4368f86 100644 --- a/src/components/CommunityPane.jsx +++ b/src/components/CommunityPane.jsx @@ -61,7 +61,7 @@ export default function CommunityPane({ }} > - + Filter practitioners by their expertise - - {/* Only show clear button if there are filters applied */} - {(filters.activities.length > 0 || - filters.sectors.length > 0 || - filters.hazards.length > 0 || - filters.size.length > 0) && ( -