diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..92a26b83
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,14 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: phlask-ecosystem
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+polar: # Replace with a single Polar username
+buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 07b55e81..0d3e7a73 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,8 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
- "editor.acceptSuggestionOnEnter": "on"
+ "editor.acceptSuggestionOnEnter": "on",
+ "[javascript]": {
+ "editor.defaultFormatter": "vscode.typescript-language-features"
+ }
}
diff --git a/cypress/e2e/desktop/filters.cy.js b/cypress/e2e/desktop/filters.cy.js
index c7469faf..03f1084b 100644
--- a/cypress/e2e/desktop/filters.cy.js
+++ b/cypress/e2e/desktop/filters.cy.js
@@ -3,47 +3,37 @@
// For each resource type, test each filter permutation and confirm only the expected number of taps appear.
describe("filters", () => {
- beforeEach(() => {
- cy.visit("/");
- // Load the form
-
- // NOTE: This line currently uses components that are due to be updated.
- // Close the tutorial modal
- cy.get('[aria-label=Close]').click()
- // Load the filter menu
- cy.get('[data-cy=button-filter-menu]').click()
- });
-
- it("should successfully show a result for each water site filter permutation", () => {
- // TODO Add an approach to test all possible filter permutations
- // Currently limiting this test to a single filtering permutation as a starting point
-
- cy.get('[data-cy="filter-option-Drinking fountain"]').click()
- cy.get('[data-cy="filter-option-ADA accessible"]').click()
- cy.get('[data-cy="filter-option-Open Access"]').click()
-
- cy.get('[data-cy=filter-apply-button]').click()
-
- // Close the filter menu
- cy.get('[data-cy=button-filter-menu]').click()
-
- // Check that a location that matches filter is present
- cy.get('[title=data-cy-2]').should('exist');
-
- // Check that a location that does not match filter is not present
- // NOTE: This is commented as this test will not current pass as filtering is not working properly.
- // cy.get('[title=data-cy-1]').should('not.exist');
- });
-
- it("should successfully show a result for each food site filter permutation", () => {
- // TODO
- });
-
- it("should successfully show a result for each foraging site filter permutation", () => {
- // TODO
- });
-
- it("should successfully show a result for each bathroom site filter permutation", () => {
- // TODO
- });
+ beforeEach(() => {
+ cy.visit("/");
+ // Load the form
+
+ // NOTE: This line currently uses components that are due to be updated.
+ // Close the tutorial modal
+ cy.get('[aria-label=Close]').click()
+ // Load the filter menu
+ cy.get('[data-cy=button-filter-menu]').click()
+ });
+
+ it("should successfully show a result for each water site filter permutation", () => {
+ // TODO Add an approach to test all possible filter permutations
+ // Currently limiting this test to a single filtering permutation as a starting point
+
+ cy.get('[data-cy="filter-option-Drinking fountain"]').click()
+ cy.get('[data-cy="filter-option-ADA accessible"]').click()
+ cy.get('[data-cy="filter-option-Open Access"]').click()
+ // Close the filter menu
+ cy.get('[data-cy=button-filter-menu]').click()
+ });
+
+ it("should successfully show a result for each food site filter permutation", () => {
+ // TODO
+ });
+
+ it("should successfully show a result for each foraging site filter permutation", () => {
+ // TODO
+ });
+
+ it("should successfully show a result for each bathroom site filter permutation", () => {
+ // TODO
+ });
});
diff --git a/public/testData.json b/public/testData.json
index 882a2fbe..5451e34f 100644
--- a/public/testData.json
+++ b/public/testData.json
@@ -17,7 +17,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"water": {
"dispenser_type": [
@@ -44,7 +48,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"water": {
"dispenser_type": [
@@ -71,7 +79,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"water": {
"dispenser_type": [
@@ -98,7 +110,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"water": {
"dispenser_type": [
@@ -125,7 +141,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"water": {
"dispenser_type": [
@@ -152,7 +172,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"water": {
"dispenser_type": [
@@ -179,7 +203,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"water": {
"dispenser_type": [
@@ -206,7 +234,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"water": {
"dispenser_type": [
@@ -237,7 +269,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"zip_code": "19149"
},
@@ -263,7 +299,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"zip_code": "19149"
},
@@ -289,7 +329,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"zip_code": "19149"
},
@@ -315,7 +359,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"zip_code": "19149"
},
@@ -341,7 +389,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"zip_code": "19149"
},
@@ -367,7 +419,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"zip_code": "19149"
},
@@ -393,7 +449,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"zip_code": "19149"
},
@@ -419,7 +479,11 @@
},
"state": "PA",
"status": "OPERATIONAL",
- "verified": false,
+ "verification": {
+ "verified": false,
+ "last_modified": "2024-03-31T00:37:46.004545+00:00",
+ "verifier": "phlask"
+ },
"version": 1,
"zip_code": "19149"
}
diff --git a/src/actions/actions.js b/src/actions/actions.js
index 71d1ee09..e2d1dfc7 100644
--- a/src/actions/actions.js
+++ b/src/actions/actions.js
@@ -18,15 +18,16 @@ export const setToggleStateFood = (toggle, toggleState) => ({
toggleState
});
-export const SET_FILTER_FUNCTION = 'SET_FILTER_FUNCTION';
-export const setFilterFunction = () => ({
- type: SET_FILTER_FUNCTION
-});
+export const setFilterFunction = createAction('SET_FILTER_FUNCTION')
+
+export const setEntryFilterFunction = createAction('SET_ENTRY_FILTER_FUNCTION')
+
+export const removeFilterFunction = createAction('REMOVE_FILTER_FUNCTION')
+
+export const removeEntryFilterFunction = createAction('REMOVE_ENTRY_FILTER_FUNCTION')
+
+export const resetFilterFunction = createAction('RESET_FILTER_FUNCTION')
-export const RESET_FILTER_FUNCTION = 'RESET_FILTER_FUNCTION';
-export const resetFilterFunction = () => ({
- type: RESET_FILTER_FUNCTION
-});
export const getResources = createAsyncThunk(
'fetch-resources',
@@ -37,7 +38,12 @@ export const getResources = createAsyncThunk(
if (process.env.REACT_APP_CYPRESS_TEST) return testData;
const snapshot = await get(ref(database, '/'));
const results = snapshot.val();
- return Object.values(results) || [];
+ return Object.entries(results).map(
+ ([id, resource]) => ({
+ ...resource,
+ id
+ })
+ );
}
);
@@ -48,6 +54,9 @@ export const pushNewResource = newResource => ({
newResource
});
+// Handles the case where an existing resource is updated from the submission form
+export const updateExistingResource = createAction('UPDATE_EXISTING_RESOURCE');
+
export const SET_USER_LOCATION = 'SET_USER_LOCATION';
export const setUserLocation = coords => ({
type: SET_USER_LOCATION,
diff --git a/src/components/AddResourceModal/AddBathroom/PageOne.jsx b/src/components/AddResourceModal/AddBathroom/PageOne.jsx
index 2cd83c77..da5fa722 100644
--- a/src/components/AddResourceModal/AddBathroom/PageOne.jsx
+++ b/src/components/AddResourceModal/AddBathroom/PageOne.jsx
@@ -17,6 +17,8 @@ import MyLocationIcon from '@mui/icons-material/MyLocation';
import useIsMobile from 'hooks/useIsMobile';
import noop from 'utils/noop';
+import { WEBSITE_REGEX } from '../utils';
+
const ENTRY_TYPE = [
{ entryType: 'Open access', explanation: 'Public site, open to all' },
{ entryType: 'Restricted', explanation: 'May not be open to all' },
@@ -195,7 +197,7 @@ const PageOne = ({
{
+ const id = result._path.pieces[0];
+ newResource.id = id;
+ dispatch(pushNewResource(newResource));
+ });
});
};
diff --git a/src/components/AddResourceModal/AddWaterTap/PageOne.jsx b/src/components/AddResourceModal/AddWaterTap/PageOne.jsx
index ff3d44b6..c82c9078 100644
--- a/src/components/AddResourceModal/AddWaterTap/PageOne.jsx
+++ b/src/components/AddResourceModal/AddWaterTap/PageOne.jsx
@@ -24,6 +24,8 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import useIsMobile from 'hooks/useIsMobile';
import noop from 'utils/noop';
+import { WEBSITE_REGEX } from '../utils';
+
const ENTRY_TYPE = [
{ entryType: 'Open access', explanation: 'Public site, open to all' },
{ entryType: 'Restricted', explanation: 'May not be open to all' },
@@ -256,7 +258,7 @@ const PageOne = ({
{
const Icon = props.icon;
@@ -95,6 +96,7 @@ export default function ChooseResource(props) {
color="#5286E9"
text="Water"
onClick={() => {
+ dispatch(resetFilterFunction())
dispatch({
type: CHANGE_RESOURCE_TYPE,
resourceType: WATER_RESOURCE_TYPE
@@ -107,6 +109,7 @@ export default function ChooseResource(props) {
color="#5DA694"
text="Foraging"
onClick={() => {
+ dispatch(resetFilterFunction())
dispatch({
type: CHANGE_RESOURCE_TYPE,
resourceType: FORAGE_RESOURCE_TYPE
@@ -119,6 +122,7 @@ export default function ChooseResource(props) {
color="#FF9A55"
text="Food"
onClick={() => {
+ dispatch(resetFilterFunction())
dispatch({
type: CHANGE_RESOURCE_TYPE,
resourceType: FOOD_RESOURCE_TYPE
@@ -131,6 +135,7 @@ export default function ChooseResource(props) {
color="#9E9E9E"
text="Bathroom"
onClick={() => {
+ dispatch(resetFilterFunction())
dispatch({
type: CHANGE_RESOURCE_TYPE,
resourceType: BATHROOM_RESOURCE_TYPE
diff --git a/src/components/Filter/Filter.js b/src/components/Filter/Filter.js
index 758ed994..9f15212b 100644
--- a/src/components/Filter/Filter.js
+++ b/src/components/Filter/Filter.js
@@ -11,6 +11,7 @@ import styles from './Filter.module.scss';
import CloseIcon from '@mui/icons-material/Close';
import IconButton from '@mui/material/IconButton';
import useIsMobile from 'hooks/useIsMobile';
+import selectFilteredResource from 'selectors/resourceSelectors';
const FilterTags = ({ tags, activeTags, resourceType, index, handleTag }) => (
@@ -24,7 +25,7 @@ const FilterTags = ({ tags, activeTags, resourceType, index, handleTag }) => (
: '')
}
onClick={() => {
- handleTag(0, resourceType, index, key);
+ handleTag(0, resourceType, tag, index, key);
}}
data-cy={`filter-option-${tag}`}
>
@@ -52,7 +53,7 @@ const FilterTagsExclusive = ({
: '')
}
onClick={() => {
- handleTag(1, resourceType, index, key);
+ handleTag(1, resourceType, tag, index, key);
}}
data-cy={`filter-option-${tag}`}
>
@@ -73,6 +74,7 @@ export default function Filter({
const isMobile = useIsMobile();
const dispatch = useDispatch();
const toolbarModal = useSelector(state => state.filterMarkers.toolbarModal);
+ const filteredResources = useSelector(state => selectFilteredResource(state));
return (
<>
{!isMobile && (
@@ -183,11 +185,14 @@ export default function Filter({
Clear All
-
-
+ Resources: {filteredResources.length}
+
diff --git a/src/components/ReactGoogleMaps/ReactGoogleMaps.js b/src/components/ReactGoogleMaps/ReactGoogleMaps.js
index 7a68f684..e594b584 100644
--- a/src/components/ReactGoogleMaps/ReactGoogleMaps.js
+++ b/src/components/ReactGoogleMaps/ReactGoogleMaps.js
@@ -9,7 +9,12 @@ import {
setUserLocation,
toggleInfoWindow,
getResources,
- setSelectedPlace
+ setSelectedPlace,
+ setFilterFunction,
+ resetFilterFunction,
+ removeFilterFunction,
+ removeEntryFilterFunction,
+ setEntryFilterFunction,
} from '../../actions/actions';
import SearchBar from '../SearchBar/SearchBar';
import SelectedTap from '../SelectedTap/SelectedTap';
@@ -21,7 +26,7 @@ import TutorialModal from '../TutorialModal/TutorialModal';
import Filter from '../Filter/Filter';
import Toolbar from '../Toolbar/Toolbar';
import phlaskMarkerIconV2 from '../icons/PhlaskMarkerIconV2';
-import selectFilteredResource from '../../selectors/waterSelectors';
+import selectFilteredResource from '../../selectors/resourceSelectors';
import useIsMobile from 'hooks/useIsMobile';
import { CITY_HALL_COORDINATES } from 'constants/defaults';
@@ -143,7 +148,7 @@ for (const [key, value] of Object.entries(filters)) {
if (category.type == 0) {
data.push(new Array(category.tags.length).fill(false));
} else {
- data.push(null);
+ data.push(category.tags.length);
}
});
noActiveFilterTags[key] = data;
@@ -154,7 +159,6 @@ export const ReactGoogleMaps = ({ google }) => {
const isMobile = useIsMobile();
const allResources = useSelector(state => state.filterMarkers.allResources);
const filteredResources = useSelector(state => selectFilteredResource(state));
-
const mapCenter = useSelector(state => state.filterMarkers.mapCenter);
const resourceType = useSelector(state => state.filterMarkers.resourceType);
const showingInfoWindow = useSelector(
@@ -250,18 +254,29 @@ export const ReactGoogleMaps = ({ google }) => {
}
};
- const handleTag = (type, filterType, index, key) => {
+ const handleTag = (type, filterType, filterTag, index, key) => {
+ //handles multi select filters
if (type == 0) {
let activeFilterTags_ = { ...activeFilterTags };
+ if (activeFilterTags_[filterType][index][key]) {
+ dispatch(removeFilterFunction({ tag: filterTag }))
+ }
+ else {
+ dispatch(setFilterFunction({ tag: filterTag }))
+ }
activeFilterTags_[filterType][index][key] =
!activeFilterTags_[filterType][index][key];
setActiveFilterTags(activeFilterTags_);
- } else if (type == 1) {
+ }
+ //handles single select entry/organization filters
+ else if (type == 1) {
let activeFilterTags_ = { ...activeFilterTags };
if (activeFilterTags_[filterType][index] == key) {
activeFilterTags_[filterType][index] = null;
+ dispatch(removeEntryFilterFunction())
} else {
- activeFilterTags_[filterType][index] = { ...key };
+ activeFilterTags_[filterType][index] = key;
+ dispatch(setEntryFilterFunction({ tag: filterTag }))
}
setActiveFilterTags(activeFilterTags_);
}
@@ -269,6 +284,7 @@ export const ReactGoogleMaps = ({ google }) => {
const clearAllTags = () => {
setActiveFilterTags(JSON.parse(JSON.stringify(noActiveFilterTags)));
+ dispatch(resetFilterFunction())
};
const applyTags = () => {
diff --git a/src/components/SelectedTapMobile/SelectedTapDetails.js b/src/components/SelectedTapMobile/SelectedTapDetails.js
index 79ab5886..e0a17b89 100644
--- a/src/components/SelectedTapMobile/SelectedTapDetails.js
+++ b/src/components/SelectedTapMobile/SelectedTapDetails.js
@@ -18,6 +18,7 @@ import {
FOOD_RESOURCE_TYPE,
FORAGE_RESOURCE_TYPE
} from '../../types/ResourceEntry';
+import VerificationButton from 'components/Verification/VerificationButton';
function SelectedTapDetails(props) {
const [pointerPositionY, setPointerPositionY] = useState(0);
@@ -36,6 +37,10 @@ function SelectedTapDetails(props) {
*/
const resource = props.selectedPlace;
+ if (resource == null || Object.keys(resource).length === 0) {
+ return ;
+ }
+
let icon;
switch (resource.resource_type) {
case WATER_RESOURCE_TYPE:
@@ -153,7 +158,17 @@ function SelectedTapDetails(props) {
{/* Currently the three dot button does nothing */}
)}
-
+
+
+
+
state.filterMarkers.allResources);
+ const filteredResources = useSelector(state => selectFilteredResource(state));
const userLocation = useSelector(state => state.filterMarkers.userLocation);
const toolbarModal = useSelector(state => state.filterMarkers.toolbarModal);
const isResourceMenuShown = useSelector(
@@ -109,32 +111,21 @@ function Toolbar({ map }) {
// NOTE: This was left as an acceptable scenario for now,
// as it is difficult for a user to do this reliably due to the popup of the location panel.
// This may be reproducible on Desktop.
- let data;
-
- switch (resourceType) {
- case WATER_RESOURCE_TYPE:
- data = allResources;
- break;
- // TODO(vontell): Filter based on requested type
- default:
- data = allResources;
- }
-
- const closest = getClosest(data, {
+ const closest = getClosest(filteredResources, {
lat: userLocation.lat,
lon: userLocation.lng
});
if (!closest) return;
- dispatch(setSelectedPlace(closest));
+ dispatch(toggleInfoWindow({
+ isShown: true,
+ infoWindowClass: isMobile ? 'info-window-in' : 'info-window-in-desktop'
+ }));
+ dispatch(setSelectedPlace(closest));
map.panTo({
lat: closest.latitude,
lng: closest.longitude
});
- toggleInfoWindow({
- isShown: true,
- infoWindowClass: isMobile ? 'info-window-in' : 'info-window-in-desktop'
- });
}
function closestButtonClicked() {
diff --git a/src/components/Verification/VerificationButton.jsx b/src/components/Verification/VerificationButton.jsx
new file mode 100644
index 00000000..2d2997c7
--- /dev/null
+++ b/src/components/Verification/VerificationButton.jsx
@@ -0,0 +1,304 @@
+import ModalWrapper from 'components/AddResourceModal/ModalWrapper';
+import React, { useState, useCallback } from 'react';
+import Button from '@mui/material/Button';
+import { getDatabase, set, ref } from 'firebase/database';
+import { resourcesConfig } from '../../firebase/firebaseConfig';
+import { initializeApp } from 'firebase/app';
+import Input from '@mui/material/Input';
+import { useDispatch } from 'react-redux';
+import {
+ updateExistingResource,
+ setSelectedPlace
+} from '../../actions/actions';
+import Dialog from '@mui/material/Dialog';
+
+const PASSWORD = 'ZnJlZXdhdGVy'; // Ask in Slack if you want the real password
+
+/**
+ * This button provides all functionality for locally verifying a resource.
+ * @param {object} props
+ * @param {ResourceEntry} props.resource The resource being verified
+ * @returns
+ */
+const VerificationButton = props => {
+ const { resource } = props;
+ const dispatch = useDispatch();
+
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [password, setPassword] = useState('');
+ const [isAdmin, setIsAdmin] = useState(false);
+ const [loginError, setLoginError] = useState('');
+ const [name, setName] = useState('');
+ const [hasBeenUpdated, setHasBeenUpdated] = useState(false);
+
+ const closeModal = useCallback(() => {
+ setIsModalOpen(false);
+ setPassword('');
+ setIsAdmin(false);
+ setHasBeenUpdated(false);
+ setLoginError('');
+ }, []);
+
+ const updateFirebaseEntry = resource => {
+ // TODO(vontell): We probably should not init this here every time, although it is likely fine.
+ const app = initializeApp(resourcesConfig);
+ const database = getDatabase(app);
+ // Removed ID since we don't want that as part of the saved data structure
+ const { id, ...filteredResource } = resource;
+ set(ref(database, `/${resource.id}`), filteredResource);
+ setHasBeenUpdated(true);
+ dispatch(updateExistingResource({resource}));
+ dispatch(setSelectedPlace(resource));
+ };
+
+ const markAsVerified = useCallback(() => {
+ const newVerification = {
+ verified: true,
+ last_modifier: name || 'phlask_app',
+ last_modified: new Date().toISOString()
+ };
+ const newResource = {
+ ...resource,
+ verification: newVerification
+ };
+ updateFirebaseEntry(newResource);
+ }, [name, resource.verification]);
+
+ const markAsUnverified = useCallback(() => {
+ const newVerification = {
+ verified: false,
+ last_modifier: name || 'phlask_app',
+ last_modified: new Date().toISOString()
+ };
+ const newResource = {
+ ...resource,
+ verification: newVerification
+ };
+ updateFirebaseEntry(newResource);
+ }, []);
+
+ const markAsInactive = useCallback(() => {
+ const newVerification = {
+ verified: false,
+ last_modifier: name || 'phlask_app',
+ last_modified: new Date().toISOString()
+ };
+ const newResource = {
+ ...resource,
+ status: 'HIDDEN',
+ verification: newVerification
+ };
+ updateFirebaseEntry(newResource);
+ }, []);
+
+ if (!resource) {
+ return;
+ }
+
+ return (
+
+
setIsModalOpen(true)} style={{ cursor: 'pointer' }}>
+ {resource.verification.verified ? 'VERIFIED' : 'UNVERIFIED'}
+
+
+
+ );
+};
+
+export default VerificationButton;
diff --git a/src/reducers/filterMarkers.js b/src/reducers/filterMarkers.js
index b86196f1..e5ba791a 100644
--- a/src/reducers/filterMarkers.js
+++ b/src/reducers/filterMarkers.js
@@ -15,19 +15,8 @@ const initialState = {
showingInfoWindow: false,
infoIsExpanded: false,
infoWindowClass: 'info-window-out-desktop',
- tapFilters: {
- filtered: false,
- handicap: false,
- sparkling: false,
- openNow: false,
- accessTypesHidden: []
- },
- foodFilters: {
- idRequired: false,
- kidOnly: false,
- openNow: false,
- accessTypesHidden: []
- },
+ filterTags: [],
+ filterEntry: "",
/** @type {ResourceEntry[]} */
allResources: [],
selectedPlace: {},
@@ -97,8 +86,39 @@ export default (state = initialState, act) => {
allResources: [...state.allResources, act.newResource]
};
- case actions.SET_FILTER_FUNCTION:
- return { filterFunction: !state.filterFunction, ...state };
+ case actions.setFilterFunction.type:
+ return { ...state, filterTags: [...state.filterTags, act.payload.tag] }
+
+ case actions.setEntryFilterFunction.type:
+ return { ...state, filterEntry: act.payload.tag }
+
+ case actions.removeFilterFunction.type:
+ return {
+ ...state,
+ filterTags: state.filterTags.filter(x => x !== act.payload.tag)
+ }
+
+ case actions.removeEntryFilterFunction.type:
+ return {
+ ...state,
+ filterEntry: ''
+ }
+
+ case actions.resetFilterFunction.type:
+ return {
+ ...state,
+ filterTags: [],
+ filterEntry: ''
+ };
+ case actions.updateExistingResource.type:
+ return {
+ ...state,
+ allResources: state.allResources.map(resource =>
+ resource.id === act.payload.resource.id
+ ? act.payload.resource
+ : resource
+ )
+ };
case actions.SET_SELECTED_PLACE:
// if passed Selected Place as an object, set selected place as the object
@@ -106,10 +126,10 @@ export default (state = initialState, act) => {
return typeof act.selectedPlace === 'object'
? { ...state, selectedPlace: act.selectedPlace }
: {
- ...state,
- selectedPlace: state.allResources[act.selectedPlace],
- showingInfoWindow: true
- };
+ ...state,
+ selectedPlace: state.allResources[act.selectedPlace],
+ showingInfoWindow: true
+ };
case actions.toggleInfoWindow.type:
return {
@@ -126,28 +146,6 @@ export default (state = initialState, act) => {
case actions.TOGGLE_INFO_EXPANDED:
return { ...state, infoIsExpanded: act.isExpanded };
- case actions.RESET_FILTER_FUNCTION:
- return {
- ...state,
- tapFilters: {
- accessTypesHidden: [],
- filtered: false,
- handicap: false,
- sparkling: false,
- openNow: false
- },
- foodFilters: {
- foodSite: false,
- school: false,
- charter: false,
- pha: false,
- idRequired: false,
- kidOnly: false,
- openNow: false,
- accessTypesHidden: []
- }
- };
-
case actions.SET_FILTERED_TAP_TYPES: {
let currentAccessTypesHidden = [...state.tapFilters.accessTypesHidden];
if (currentAccessTypesHidden.includes(act.tapType)) {
diff --git a/src/selectors/resourceSelectors.js b/src/selectors/resourceSelectors.js
new file mode 100644
index 00000000..9bc629cd
--- /dev/null
+++ b/src/selectors/resourceSelectors.js
@@ -0,0 +1,102 @@
+import { createSelector } from 'reselect';
+
+const getAllResources = state => state.filterMarkers.allResources;
+const getResourceType = state => state.filterMarkers.resourceType;
+const filterTags = state => state.filterMarkers.filterTags;
+const filterEntry = state => state.filterMarkers.filterEntry;
+
+//this controls the mapping between values shown in the frontend and values used in the database, keys correspond to frontend and values to the database
+const tagMapping = {
+ //ENTRY TYPES
+ 'Open Access': 'OPEN',
+ 'Restricted': 'RESTRICTED',
+ 'Unsure': 'UNSURE',
+ //WATER
+ //dispenser types
+ 'Drinking fountain': 'DRINKING_FOUNTAIN',
+ 'Bottle filler': 'BOTTLE_FILLER',
+ 'Sink': 'SINK',
+ 'Water cooler': 'WATER_COOLER',
+ 'Soda machine': 'SODA_MACHINE',
+ 'Vessel': 'VESSEL',
+ //features
+ 'ADA accessible': 'WHEELCHAIR_ACCESSIBLE',
+ 'Filtered water': 'FILTERED',
+ 'Vessel needed': 'BYOB',
+ 'ID required': 'ID_REQUIRED',
+ //FOOD
+ //food types
+ 'Perishable': 'PERISHABLE',
+ 'Non-perishable': 'NON_PERISHABLE',
+ 'Prepared foods and meals': 'PREPARED',
+ //distribution types
+ 'Eat on site': 'EAT_ON_SITE',
+ 'Delivery': 'DELIVERY',
+ 'Pick up': 'PICKUP',
+ //organization types
+ 'Government': 'GOVERNMENT', 'Business': 'BUSINESS', 'Non-profit': 'NON_PROFIT',
+ //FORAGING
+ //forage type
+ 'Nut': 'NUT', 'Fruit': 'FRUIT', 'Leaves': 'LEAVES', 'Bark': 'BARK', 'Flowers': 'FLOWERS',
+ //features
+ 'Medicinal': 'MEDICINAL', 'In season': 'IN_SEASON', 'Community garden': 'COMMUNITY_GARDEN',
+ //BATHROOM
+ //features
+ 'Gender neutral': 'GENDER_NEUTRAL',
+ 'Changing table': 'CHANGING_TABLE',
+ 'Single occupancy': 'SINGLE_OCCUPANCY',
+ 'Family bathroom': 'FAMILY',
+ 'Has water fountain': 'HAS_FOUNTAIN',
+}
+
+const propertyMapping = {
+ 'WATER': ['dispenser_type', 'tags'],
+ 'FOOD': ['food_type', 'distribution_type'],
+ 'FORAGE': ['forage_type', 'tags'],
+ 'BATHROOM': ['tags']
+}
+
+/**
+ * This creates a selector for all resources filtered by the requested filters.
+ */
+const selectFilteredResource = createSelector(
+ [getAllResources, getResourceType, filterTags, filterEntry],
+ (allResources, resourceType, tags, entryType) => {
+ // First, filter based on resource type
+ let filteredResources = []
+ allResources.filter(resource => {
+ let isValid = false
+ let isEntryValid = true
+ if (resource.resource_type === resourceType) isValid = true
+ //run through filters
+ if (isValid) {
+ //logic for entry types
+ if (entryType) {
+ if (resource.entry_type != tagMapping[entryType]) isEntryValid = false
+ }
+ const resourceTypeFormatted = resource.resource_type.toLowerCase()
+ //logic for other filters
+ tags.forEach(tag => {
+ const tagFormatted = tagMapping[tag]
+ //check to make sure resource has property with filter info
+ if (resourceTypeFormatted in resource) {
+ let tagFound = false
+ //loop through filter info types to find filter value
+ propertyMapping[resourceType].forEach(prop => {
+ if (prop in resource[resourceTypeFormatted]) {
+ if (resource[resourceTypeFormatted][prop].includes(tagFormatted)) tagFound = true
+ }
+ })
+ if (!tagFound) isValid = false
+ }
+ else if (!(resourceTypeFormatted in resource)) isValid = false
+ })
+ }
+ //return resource if it matches resource type and entry type filters
+ if (isValid && isEntryValid) filteredResources.push(resource)
+ });
+ return filteredResources
+ }
+);
+
+export default selectFilteredResource;
diff --git a/src/selectors/waterSelectors.js b/src/selectors/waterSelectors.js
deleted file mode 100644
index 92cd8ca7..00000000
--- a/src/selectors/waterSelectors.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import { createSelector } from 'reselect';
-
-import { hours } from '../helpers/hours';
-
-const getTapFilters = state => state.filterMarkers.tapFilters;
-const getAllResources = state => state.filterMarkers.allResources;
-const getResourceType = state => state.filterMarkers.resourceType;
-
-/**
- * This creates a selector for all resources filtered by the requested filters.
- */
-const selectFilteredResource = createSelector(
- [getAllResources, getResourceType],
- (allResources, resourceType) => {
- // First, filter based on resource
- return allResources.filter(resource => {
- return resource.resource_type === resourceType;
- });
-
- // // Default filters
- // filteredTaps = Object.keys(filteredTaps)
- // .filter(key => {
- // return (
- // allTaps[key].permanently_closed !== undefined &&
- // allTaps[key].permanently_closed !== true
- // );
- // })
- // .reduce((obj, key) => {
- // obj[key] = allTaps[key];
- // return obj;
- // }, []);
- //
- // // If we want to filter for filtered taps (water filter)
- // if (tapFilters.filtered) {
- // filteredTaps = Object.keys(filteredTaps)
- // .filter(key => allTaps[key].filtration === 'Yes')
- // .reduce((obj, key) => {
- // obj[key] = allTaps[key];
- // return obj;
- // }, []);
- // }
- //
- // // If we want to filter for handicap-accessible taps
- // if (tapFilters.handicap) {
- // filteredTaps = Object.keys(filteredTaps)
- // .filter(key => filteredTaps[key].handicap === 'Yes')
- // .reduce((obj, key) => {
- // obj[key] = filteredTaps[key];
- // return obj;
- // }, []);
- // }
- //
- // // If we want to filter for taps that offer sparkling water
- // if (tapFilters.sparkling) {
- // filteredTaps = Object.keys(filteredTaps)
- // .filter(key => filteredTaps[key].sparkling === 'yes')
- // .reduce((obj, key) => {
- // obj[key] = filteredTaps[key];
- // return obj;
- // }, []);
- // }
- //
- // if (tapFilters.openNow) {
- // filteredTaps = Object.keys(filteredTaps)
- // .filter(key => {
- // const today = new Date();
- // const currentDay = today.getDay();
- //
- // let selectedPlace = filteredTaps[key];
- //
- // return selectedPlace.hours !== undefined
- // ? selectedPlace.hours.length >= currentDay + 1
- // ? selectedPlace.hours[currentDay].close !== undefined &&
- // selectedPlace.hours[currentDay].open !== undefined
- // ? hours.checkOpen(
- // selectedPlace.hours[currentDay].open.time,
- // selectedPlace.hours[currentDay].close.time
- // )
- // : false
- // : false
- // : false;
- // })
- // .reduce((obj, key) => {
- // obj[key] = filteredTaps[key];
- // return obj;
- // }, []);
- // }
- //
- // filteredTaps = Object.keys(filteredTaps)
- // .filter(
- // key => !tapFilters.accessTypesHidden.includes(filteredTaps[key].access)
- // )
- // .reduce((obj, key) => {
- // obj[key] = filteredTaps[key];
- // return obj;
- // }, []);
- // return filteredTaps;
- // });
- }
-);
-
-export default selectFilteredResource;
diff --git a/src/types/ResourceEntry.js b/src/types/ResourceEntry.js
index 7c625e2b..96025ff8 100644
--- a/src/types/ResourceEntry.js
+++ b/src/types/ResourceEntry.js
@@ -19,7 +19,7 @@
* @property {string} last_modified The date this resource was last modified, in ISO UTC format.
* @property {string} last_modifier Who last modified this resource.
* @property {DataSource} source Where this resource data came from.
- * @property {boolean} verified Whether or not this resource is currently verified.
+ * @property {Verification} verification The verification details of this resource.
* @property {"WATER" | "FOOD" | "FORAGE" | "BATHROOM"} resource_type The type of resource.
* @property {string | undefined} address The street address of the resource (not including city, state, or zip). May include the secondary address.
* @property {string} city The city of the resource.
@@ -32,7 +32,7 @@
* @property {string | undefined} guidelines Any additional community guidelines or rules for this resource.
* @property {string | undefined} description A description of the resource.
* @property {string} name A non-address name for this location, such as the business name or park name.
- * @property {"OPERATIONAL" | "TEMPORARILY_CLOSED" | "PERMANENTLY_CLOSED"} status The current status of this resource.
+ * @property {"OPERATIONAL" | "TEMPORARILY_CLOSED" | "PERMANENTLY_CLOSED" | "HIDDEN"} status The current status of this resource.
* @property {"OPEN" | "RESTRICTED" | "UNSURE" | undefined} entry_type What entry permissions are required for this resource.
* @property {GooglePlacesPeriod[] | undefined} hours The hours of operation for this resource, if available.
* @property {WaterInfo | undefined} water If the resource_type is WATER, the information about the water resource.
@@ -94,7 +94,15 @@
* @property {("WHEELCHAIR_ACCESSIBLE" | "GENDER_NEUTRAL" | "CHANGING_TABLE" | "SINGLE_OCCUPANCY" | "FAMILY")[]} tags A list of additional tags regarding this bathroom resource. Can be empty.
*/
-export const WATER_RESOURCE_TYPE = "WATER";
-export const FOOD_RESOURCE_TYPE = "FOOD";
-export const FORAGE_RESOURCE_TYPE = "FORAGE";
-export const BATHROOM_RESOURCE_TYPE = "BATHROOM";
\ No newline at end of file
+/**
+ * Details for verification status.
+ * @typedef {Object} Verification
+ * @property {boolean} verified Whether or not this resource is currently verified.
+ * @property {Date} last_modified The latest date this resource had a verification change.
+ * @property {string} verifier Who most recently changes the verification state of this resource.
+ */
+
+export const WATER_RESOURCE_TYPE = 'WATER';
+export const FOOD_RESOURCE_TYPE = 'FOOD';
+export const FORAGE_RESOURCE_TYPE = 'FORAGE';
+export const BATHROOM_RESOURCE_TYPE = 'BATHROOM';