From aeeacf407bf31f0f293c0deeb2d3427e7bfc28f8 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 5 Dec 2024 13:42:59 -0500 Subject: [PATCH 1/3] new landing page --- src/components/DropDownSelector.jsx | 14 +- src/components/PractitionerCard.jsx | 115 ++++++ src/pages/LandingPage.jsx | 615 +++++++++++++++++++++++++--- src/pages/OldLandingPage.jsx | 83 ++++ src/pages/PractitionerListPage.jsx | 132 +----- src/util/api.js | 68 ++- 6 files changed, 831 insertions(+), 196 deletions(-) create mode 100644 src/components/PractitionerCard.jsx create mode 100644 src/pages/OldLandingPage.jsx diff --git a/src/components/DropDownSelector.jsx b/src/components/DropDownSelector.jsx index cbbf2d2..cdb227c 100644 --- a/src/components/DropDownSelector.jsx +++ b/src/components/DropDownSelector.jsx @@ -14,7 +14,7 @@ const AddButton = styled(Button)(({ theme }) => ({ borderRadius: '4px', textTransform: 'none', justifyContent: 'flex-start', - width: '300px', // Increased width + width: '400px', '&:hover': { backgroundColor: '#1976d2', }, @@ -25,7 +25,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', @@ -78,6 +78,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 (
+} > - Add another {option} + Add another {trimmedOption.toLowerCase()} 200 ? description.substring(0, 200) + '...' : description; + + // Only take first 3 activities for display + const displayedActivities = practitioner.activities.slice(0, 3); + + return ( + + + {/* Logo Container */} + + Company Logo + + + + {practitioner.org} + + + + {truncatedDescription} + + + + + Expertise + + + {displayedActivities.map((activity, index) => ( + + ))} + + + + + + + + + ); +} 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 - + /> + ))} + + + + + {availableChoices.map((option) => ( + handleSelect(option)} + sx={{ + '&:hover': { + bgcolor: 'primary.tan', + }, + }} + > + {option} + + ))} + + +
+ ); +}; + +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? + - + + Where is your community? + + + `${option.city}, ${option.state}`} + sx={{ flexGrow: 1 }} + renderInput={(params) => ( + , + }} + /> + )} + /> - + )} + + + {/* 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 && ( + + + + )} + + )} + + ); } 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

+ + + + + + + +
+
+ ); +} 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 */} - - Company Logo - - - - {practitioner.org} - - - - {truncatedDescription} - - - - - Expertise - - - {displayedActivities.map((activity, index) => ( - - ))} - - - - - - - - - ); -} - -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; From 401f0e31262d7ca7404ee79021dc6e1fd7bc27a7 Mon Sep 17 00:00:00 2001 From: Jeff Bliss Date: Thu, 5 Dec 2024 13:52:08 -0500 Subject: [PATCH 2/3] small update to drop down --- src/components/DropDownSelector.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/DropDownSelector.jsx b/src/components/DropDownSelector.jsx index 9b0c567..3f9a355 100644 --- a/src/components/DropDownSelector.jsx +++ b/src/components/DropDownSelector.jsx @@ -96,12 +96,11 @@ const DropDownSelector = ({ availableSelections, selections, setSelections, opti aria-expanded={open ? 'true' : undefined} onClick={handleClick} startIcon={+} - sx={{ width: "100%" }} + sx={{ width: '100%' }} > - + Add another {trimmedOption.toLowerCase()} - {option} Date: Thu, 5 Dec 2024 14:07:36 -0500 Subject: [PATCH 3/3] updates to PractitionerCard --- src/components/PractitionerCard.jsx | 62 +++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/src/components/PractitionerCard.jsx b/src/components/PractitionerCard.jsx index 6146d8e..12f556e 100644 --- a/src/components/PractitionerCard.jsx +++ b/src/components/PractitionerCard.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Card, CardContent, Typography, Box, Button, Chip, Stack } from '@mui/material'; +import { Card, CardContent, Typography, Box, Button, Stack, Checkbox, FormControlLabel } from '@mui/material'; import PersonIcon from '@mui/icons-material/Person'; import climatePracLogo from '../assets/climate_prac.png'; import theme from '../theme'; @@ -7,8 +7,6 @@ import theme from '../theme'; export default 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 for display const displayedActivities = practitioner.activities.slice(0, 3); return ( @@ -22,7 +20,6 @@ export default function PractitionerCard({ practitioner }) { }} > - {/* Logo Container */} - Expertise + Adaptation Expertise - {displayedActivities.map((activity, index) => ( - + > + {activity} + ))} - + @@ -101,6 +108,7 @@ export default function PractitionerCard({ practitioner }) { backgroundColor: theme.palette.primary.midBlue, borderRadius: 8, textTransform: 'none', + mb: 2, '&:hover': { backgroundColor: theme.palette.primary.main, }, @@ -108,6 +116,26 @@ export default function PractitionerCard({ practitioner }) { > Full Practitioner Org Profile + + + } + label="Compare this practitioner" + sx={{ + '& .MuiFormControlLabel-label': { + fontSize: '0.875rem', + color: theme.palette.primary.main, + }, + }} + />