diff --git a/src/actions/actions.js b/src/actions/actions.js index a1441e31..f7339808 100644 --- a/src/actions/actions.js +++ b/src/actions/actions.js @@ -77,6 +77,7 @@ export const setTapInfoOpenedWhileSearchOpen = createAction( ); export const setToolbarModal = createAction('SET_TOOLBAR_MODAL'); +export const setEditingResource = createAction('SET_EDITING_RESOURCE'); export const TOOLBAR_MODAL_NONE = 'TOOLBAR_MODAL_NONE'; export const TOOLBAR_MODAL_RESOURCE = 'TOOLBAR_MODAL_RESOURCE'; export const TOOLBAR_MODAL_FILTER = 'TOOLBAR_MODAL_FILTER'; diff --git a/src/components/AddResourceModal/AddBathroom/AddBathroom.jsx b/src/components/AddResourceModal/AddBathroom/AddBathroom.jsx index 1e107618..065ba498 100644 --- a/src/components/AddResourceModal/AddBathroom/AddBathroom.jsx +++ b/src/components/AddResourceModal/AddBathroom/AddBathroom.jsx @@ -32,7 +32,8 @@ const AddBathroom = ({ hasFountain, checkboxChangeHandler, textFieldChangeHandler, - isValidAddress + isValidAddress, + editMode }) => { const isMobile = useIsMobile(); const userLocation = useSelector(getUserLocation); @@ -51,8 +52,25 @@ const AddBathroom = ({ setValue, trigger, control, + reset, formState: { errors } - } = useForm(); + } = useForm({ + defaultValues: { + name, + address, + website, + description + } + }); + + useEffect(() => { + reset({ + name, + address, + website, + description + }); + }, [name, address, website, description, reset]); const requiredFieldMsg = ( @@ -90,7 +108,7 @@ const AddBathroom = ({ }) }} > - Add a Bathroom Resource + {editMode ? 'Edit Bathroom Resource' : 'Add a Bathroom Resource'} )} {(page === 1 || isMobile) && ( diff --git a/src/components/AddResourceModal/AddBathroom/PageOne.jsx b/src/components/AddResourceModal/AddBathroom/PageOne.jsx index 5eb14b27..d64e58e0 100644 --- a/src/components/AddResourceModal/AddBathroom/PageOne.jsx +++ b/src/components/AddResourceModal/AddBathroom/PageOne.jsx @@ -39,7 +39,8 @@ const PageOne = ({ control, setValue, textFieldChangeHandler, - isValidAddress + isValidAddress, + editMode }) => { const isMobile = useIsMobile(); diff --git a/src/components/AddResourceModal/AddFood/AddFood.jsx b/src/components/AddResourceModal/AddFood/AddFood.jsx index 2142b11a..1c3c09a4 100644 --- a/src/components/AddResourceModal/AddFood/AddFood.jsx +++ b/src/components/AddResourceModal/AddFood/AddFood.jsx @@ -45,7 +45,8 @@ const AddFood = ({ guidelines, checkboxChangeHandler, textFieldChangeHandler, - isValidAddress + isValidAddress, + editMode }) => { const isMobile = useIsMobile(); const getVariableName = variable => Object.keys(variable)[0]; @@ -66,8 +67,25 @@ const AddFood = ({ setValue, trigger, control, + reset, formState: { errors } - } = useForm(); + } = useForm({ + defaultValues: { + name, + address, + website, + description + } + }); + + useEffect(() => { + reset({ + name, + address, + website, + description + }); + }, [name, address, website, description, reset]); const requiredFieldMsg = ( @@ -110,7 +128,7 @@ const AddFood = ({ }) }} > - Add a Food Resource + {editMode ? 'Edit Food Resource' : 'Add a Food Resource'} )} {(page === 1 || isMobile) && ( diff --git a/src/components/AddResourceModal/AddFood/PageOne.jsx b/src/components/AddResourceModal/AddFood/PageOne.jsx index 30a01c1c..75446cb9 100644 --- a/src/components/AddResourceModal/AddFood/PageOne.jsx +++ b/src/components/AddResourceModal/AddFood/PageOne.jsx @@ -50,7 +50,8 @@ const PageOne = ({ getVariableName, checkboxChangeHandler, textFieldChangeHandler, - isValidAddress + isValidAddress, + editMode }) => { const isMobile = useIsMobile(); diff --git a/src/components/AddResourceModal/AddForaging/AddForaging.jsx b/src/components/AddResourceModal/AddForaging/AddForaging.jsx index 83d0f38d..a96fa6b5 100644 --- a/src/components/AddResourceModal/AddForaging/AddForaging.jsx +++ b/src/components/AddResourceModal/AddForaging/AddForaging.jsx @@ -44,7 +44,8 @@ const AddForaging = ({ communityGarden, checkboxChangeHandler, textFieldChangeHandler, - isValidAddress + isValidAddress, + editMode }) => { const isMobile = useIsMobile(); const userLocation = useSelector(getUserLocation); @@ -63,8 +64,25 @@ const AddForaging = ({ setValue, trigger, control, + reset, formState: { errors } - } = useForm(); + } = useForm({ + defaultValues: { + name, + address, + website, + description + } + }); + + useEffect(() => { + reset({ + name, + address, + website, + description + }); + }, [name, address, website, description, reset]); const requiredFieldMsg = ( @@ -109,7 +127,7 @@ const AddForaging = ({ }) }} > - Add a Foraging Resource + {editMode ? 'Edit Foraging Resource' : 'Add a Foraging Resource'} )} {(page === 1 || isMobile) && ( diff --git a/src/components/AddResourceModal/AddForaging/PageOne.jsx b/src/components/AddResourceModal/AddForaging/PageOne.jsx index e1f02dc9..b3dff7f7 100644 --- a/src/components/AddResourceModal/AddForaging/PageOne.jsx +++ b/src/components/AddResourceModal/AddForaging/PageOne.jsx @@ -53,7 +53,8 @@ const PageOne = ({ getVariableName, checkboxChangeHandler, textFieldChangeHandler, - isValidAddress + isValidAddress, + editMode }) => { const isMobile = useIsMobile(); diff --git a/src/components/AddResourceModal/AddResourceModalV2.jsx b/src/components/AddResourceModal/AddResourceModalV2.jsx index 7f201943..0b8d8a1e 100644 --- a/src/components/AddResourceModal/AddResourceModalV2.jsx +++ b/src/components/AddResourceModal/AddResourceModalV2.jsx @@ -1,11 +1,11 @@ -import { useMemo, useState } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { geocodeByAddress, getLatLng } from 'react-places-autocomplete'; import CloseIcon from '@mui/icons-material/Close'; import { IconButton } from '@mui/material'; import noop from 'utils/noop'; import useIsMobile from 'hooks/useIsMobile'; -import { TOOLBAR_MODAL_NONE, pushNewResource } from 'actions/actions'; +import { TOOLBAR_MODAL_NONE, pushNewResource, setEditingResource } from 'actions/actions'; import debounce from 'utils/debounce'; @@ -86,11 +86,128 @@ const initialState = { hasFountain: false }; +const WATER_TAG_TO_FIELD = { + FILTERED: 'filtration', + BYOB: 'waterVesselNeeded', + ID_REQUIRED: 'idRequired', + WHEELCHAIR_ACCESSIBLE: 'handicapAccessible' +}; + +const WATER_DISPENSER_TO_FIELD = { + DRINKING_FOUNTAIN: 'drinkingFountain', + BOTTLE_FILLER: 'bottleFillerAndFountain', + SINK: 'sink', + JUG: 'waterJug', + SODA_MACHINE: 'sodaMachine', + PITCHER: 'pitcher', + WATER_COOLER: 'waterCooler' +}; + +const FOOD_TYPE_TO_FIELD = { + PERISHABLE: 'perishable', + NON_PERISHABLE: 'nonPerishable', + PREPARED: 'prepared' +}; + +const FOOD_DISTRIBUTION_TO_FIELD = { + EAT_ON_SITE: 'eatOnSite', + DELIVERY: 'delivery', + PICKUP: 'pickUp' +}; + +const FORAGE_TYPE_TO_FIELD = { + NUT: 'nut', + FRUIT: 'fruit', + LEAVES: 'leaves', + BARK: 'bark', + FLOWERS: 'flowers', + ROOT: 'root' +}; + +const FORAGE_TAG_TO_FIELD = { + MEDICINAL: 'medicinal', + IN_SEASON: 'inSeason', + COMMUNITY_GARDEN: 'communityGarden' +}; + +const BATHROOM_TAG_TO_FIELD = { + WHEELCHAIR_ACCESSIBLE: 'handicapAccessible', + GENDER_NEUTRAL: 'genderNeutral', + CHANGING_TABLE: 'changingTable', + SINGLE_OCCUPANCY: 'singleOccupancy', + FAMILY: 'familyBathroom', + HAS_FOUNTAIN: 'hasFountain' +}; + +const mapArrayToFields = (array, mapping) => { + if (!array) return {}; + return array.reduce((acc, item) => { + const field = mapping[item]; + return field ? { ...acc, [field]: true } : acc; + }, {}); +}; + +const getStandardResourceValues = resource => ({ + name: resource.name || '', + address: resource.address || '', + website: resource.website || '', + description: resource.description || '', + guidelines: resource.guidelines || '', + entryType: resource.entry_type || '', + latitude: resource.latitude, + longitude: resource.longitude, + isValidAddress: true, + pictures: [], + images: [], + handicapAccessible: false, + idRequired: false +}); + +const getWaterResourceValues = resource => ({ + ...mapArrayToFields(resource.water?.tags, WATER_TAG_TO_FIELD), + ...mapArrayToFields(resource.water?.dispenser_type, WATER_DISPENSER_TO_FIELD) +}); + +const getFoodResourceValues = resource => ({ + ...mapArrayToFields(resource.food?.food_type, FOOD_TYPE_TO_FIELD), + ...mapArrayToFields(resource.food?.distribution_type, FOOD_DISTRIBUTION_TO_FIELD), + organization: resource.food?.organization_name || '' +}); + +const getForageResourceValues = resource => ({ + ...mapArrayToFields(resource.forage?.forage_type, FORAGE_TYPE_TO_FIELD), + ...mapArrayToFields(resource.forage?.tags, FORAGE_TAG_TO_FIELD) +}); + +const getBathroomResourceValues = resource => + mapArrayToFields(resource.bathroom?.tags, BATHROOM_TAG_TO_FIELD); + +const mapResourceToFormState = resource => { + if (!resource) return initialState; + + const state = getStandardResourceValues(resource); + + switch (resource.resource_type) { + case WATER_RESOURCE_TYPE: + return { ...initialState, ...state, ...getWaterResourceValues(resource) }; + case FOOD_RESOURCE_TYPE: + return { ...initialState, ...state, ...getFoodResourceValues(resource) }; + case FORAGE_RESOURCE_TYPE: + return { ...initialState, ...state, ...getForageResourceValues(resource) }; + case BATHROOM_RESOURCE_TYPE: + return { ...initialState, ...state, ...getBathroomResourceValues(resource) }; + default: + return { ...initialState, ...state }; + } +}; + const AddResourceModalV2 = () => { const [page, setPage] = useState(0); const [resourceForm, setResourceForm] = useState(null); const isMobile = useIsMobile(); + const editingResource = useSelector(state => state.filterMarkers.editingResource); + const isEditMode = !!editingResource; const onPageChange = update => { setPage(prev => { @@ -102,7 +219,12 @@ const AddResourceModalV2 = () => { }); }; - const [values, setValues] = useState(initialState); + const [values, setValues] = useState(() => { + if (isEditMode && editingResource) { + return mapResourceToFormState(editingResource); + } + return initialState; + }); const dispatch = useDispatch(); const userLocation = useSelector(getUserLocation); @@ -110,6 +232,13 @@ const AddResourceModalV2 = () => { dispatch({ type: 'SET_TOOLBAR_MODAL', modal }); }; + useEffect(() => { + if (isEditMode && editingResource) { + setResourceForm(editingResource.resource_type); + setValues(mapResourceToFormState(editingResource)); + } + }, [isEditMode, editingResource]); + const checkboxChangeHandler = e => { setValues(prevValues => ({ ...prevValues, @@ -342,15 +471,24 @@ const AddResourceModalV2 = () => { }; } - addResource(newResource).then(result => { - dispatch(pushNewResource(newResource)); - }); + // If editing an existing resource, we should submit as a suggestion instead of creating a new resource + // TODO: Implement suggestion submission workflow with admin review/approval system + if (isEditMode && editingResource?.id) { + // For now, editing is disabled - awaiting suggestion workflow implementation + dispatch(setEditingResource(null)); + } else { + // Adding a new resource + addResource(newResource).then(_result => { + dispatch(pushNewResource(newResource)); + }); + } }); }; const onExitedWrapper = () => { setValues(initialState); setResourceForm(null); + dispatch(setEditingResource(null)); }; const handleClose = () => { @@ -424,6 +562,7 @@ const AddResourceModalV2 = () => { checkboxChangeHandler={checkboxChangeHandler} textFieldChangeHandler={textFieldChangeHandler} isValidAddress={values.isValidAddress} + editMode={isEditMode} /> )} @@ -454,6 +593,7 @@ const AddResourceModalV2 = () => { checkboxChangeHandler={checkboxChangeHandler} textFieldChangeHandler={textFieldChangeHandler} isValidAddress={values.isValidAddress} + editMode={isEditMode} /> )} @@ -478,6 +618,7 @@ const AddResourceModalV2 = () => { checkboxChangeHandler={checkboxChangeHandler} textFieldChangeHandler={textFieldChangeHandler} isValidAddress={values.isValidAddress} + editMode={isEditMode} /> )} @@ -505,6 +646,7 @@ const AddResourceModalV2 = () => { checkboxChangeHandler={checkboxChangeHandler} textFieldChangeHandler={textFieldChangeHandler} isValidAddress={values.isValidAddress} + editMode={isEditMode} /> )} diff --git a/src/components/AddResourceModal/AddWaterTap/AddWaterTap.jsx b/src/components/AddResourceModal/AddWaterTap/AddWaterTap.jsx index 861974c1..e4904c65 100644 --- a/src/components/AddResourceModal/AddWaterTap/AddWaterTap.jsx +++ b/src/components/AddResourceModal/AddWaterTap/AddWaterTap.jsx @@ -40,7 +40,8 @@ const AddWaterTap = ({ guidelines, checkboxChangeHandler, textFieldChangeHandler, - isValidAddress + isValidAddress, + editMode }) => { const isMobile = useIsMobile(); const userLocation = useSelector(getUserLocation); @@ -60,7 +61,14 @@ const AddWaterTap = ({ trigger, control, formState: { errors } - } = useForm(); + } = useForm({ + defaultValues: { + name, + address, + website, + description + } + }); const requiredFieldMsg = ( @@ -100,7 +108,7 @@ const AddWaterTap = ({ }) }} > - Add a Water Resource + {editMode ? 'Edit Water Resource' : 'Add a Water Resource'} )} {(page === 1 || isMobile) && ( diff --git a/src/components/AddResourceModal/AddWaterTap/PageOne.jsx b/src/components/AddResourceModal/AddWaterTap/PageOne.jsx index 3100263c..968b8469 100644 --- a/src/components/AddResourceModal/AddWaterTap/PageOne.jsx +++ b/src/components/AddResourceModal/AddWaterTap/PageOne.jsx @@ -57,7 +57,8 @@ const PageOne = ({ getVariableName, checkboxChangeHandler, textFieldChangeHandler, - isValidAddress + isValidAddress, + editMode }) => { const isMobile = useIsMobile(); diff --git a/src/components/ReactGoogleMaps/ReactGoogleMaps.jsx b/src/components/ReactGoogleMaps/ReactGoogleMaps.jsx index f770056f..b1a4bab1 100644 --- a/src/components/ReactGoogleMaps/ReactGoogleMaps.jsx +++ b/src/components/ReactGoogleMaps/ReactGoogleMaps.jsx @@ -171,6 +171,7 @@ const ReactGoogleMaps = () => { state => state.filterMarkers.searchBarMapTintOn ); const userLocation = useSelector(getUserLocation); + const editingResource = useSelector(state => state.filterMarkers.editingResource); const [searchedTap, setSearchedTap] = useState(null); const [map, setMap] = useState(null); const [activeFilterTags, setActiveFilterTags] = useState( @@ -190,6 +191,9 @@ const ReactGoogleMaps = () => { // toggle window goes here const onMarkerClick = resource => { + // Prevent switching resources while editing + if (editingResource) return; + dispatch( toggleInfoWindow({ isShown: true, diff --git a/src/components/SelectedTap/SelectedTap.jsx b/src/components/SelectedTap/SelectedTap.jsx index 22b59e55..c333ede4 100644 --- a/src/components/SelectedTap/SelectedTap.jsx +++ b/src/components/SelectedTap/SelectedTap.jsx @@ -32,8 +32,6 @@ const SelectedTap = () => { const [walkingDuration, setWalkingDuration] = useState(0); const [infoCollapseMobile, setInfoCollapseMobile] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [editingResource, setEditingResource] = useState(null); const showingInfoWindow = useSelector( state => state.filterMarkers.showingInfoWindow @@ -84,11 +82,6 @@ const SelectedTap = () => { dispatch(toggleInfoExpanded(shouldExpand)); }; - const handleStartEdit = resource => { - setIsEditing(true); - setEditingResource(resource); - }; - const handleToggleInfoWindow = isShown => { let infoWindowClass = 'info-window-'; infoWindowClass += isShown ? 'in' : 'out'; @@ -193,11 +186,6 @@ const SelectedTap = () => { infoCollapse={infoCollapseMobile} setInfoCollapse={setInfoCollapseMobile} isMobile - isEditing={isEditing} - setIsEditing={setIsEditing} - editingResource={editingResource} - setEditingResource={setEditingResource} - onStartEdit={handleStartEdit} > { setInfoCollapse={setInfoCollapseMobile} isMobile={false} closeModal={() => handleToggleInfoWindow(false)} - isEditing={isEditing} - setIsEditing={setIsEditing} - editingResource={editingResource} - setEditingResource={setEditingResource} - onStartEdit={handleStartEdit} > diff --git a/src/components/SelectedTapDetails/SelectedTapDetails.jsx b/src/components/SelectedTapDetails/SelectedTapDetails.jsx index c61622b5..05d8ac13 100644 --- a/src/components/SelectedTapDetails/SelectedTapDetails.jsx +++ b/src/components/SelectedTapDetails/SelectedTapDetails.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useDispatch } from 'react-redux'; import { Button, Collapse, IconButton, SvgIcon, Menu, MenuItem } from '@mui/material'; import { styled } from '@mui/material/styles'; import CloseIcon from '@mui/icons-material/Close'; @@ -7,6 +8,7 @@ import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import DirectionIcon from 'icons/ArrowElbowUpRight'; import CaretDownSvg from 'icons/CaretDown'; import ExportSvg from 'icons/Export'; +import { setEditingResource, setToolbarModal, TOOLBAR_MODAL_CONTRIBUTE } from 'actions/actions'; import FountainIcon from 'icons/CircleWaterIcon'; import ForagingIcon from 'icons/CircleForagingIcon'; @@ -125,13 +127,9 @@ const SelectedTapDetails = ({ isMobile, closeModal, selectedPlace, - children, - isEditing, - setIsEditing, - editingResource, - setEditingResource, - onStartEdit + children }) => { + const dispatch = useDispatch(); const [menuAnchor, setMenuAnchor] = React.useState(null); const handleMenuOpen = (event) => { @@ -143,7 +141,8 @@ const SelectedTapDetails = ({ }; const handleSuggestEdit = () => { - onStartEdit(selectedPlace); + dispatch(setEditingResource(selectedPlace)); + dispatch(setToolbarModal(TOOLBAR_MODAL_CONTRIBUTE)); handleMenuClose(); }; diff --git a/src/db.js b/src/db.js index 73462ed5..6354ed41 100644 --- a/src/db.js +++ b/src/db.js @@ -63,4 +63,18 @@ export const getContributors = async () => { return data; } +/** + * Submit a suggestion for editing an existing resource + * TODO: Implement suggestion submission workflow with admin review/approval system + * TODO: Define and implement the full suggestion workflow: + * 1. Store suggestion in database + * 2. Create admin review/approval interface + * 3. Define how approved suggestions are applied to resources + * @param {string} resourceId The ID of the resource being suggested for edit + * @param {Object} suggestionData The suggested changes to the resource + * @returns {Promise} The submitted suggestion + */ +// eslint-disable-next-line no-unused-vars +export const submitSuggestion = async (resourceId, suggestionData) => null; + export default {}; \ No newline at end of file diff --git a/src/reducers/filterMarkers.js b/src/reducers/filterMarkers.js index c77b9094..d3ff8b42 100644 --- a/src/reducers/filterMarkers.js +++ b/src/reducers/filterMarkers.js @@ -22,6 +22,7 @@ const initialState = { allResources: [], selectedPlace: {}, toolbarModal: actions.TOOLBAR_MODAL_NONE, + editingResource: null, setSearchBarMapTintOn: false, tapInfoOpenedWhileSearchOpen: false, resourceType: WATER_RESOURCE_TYPE @@ -196,6 +197,9 @@ export default (state = initialState, act = {}) => { case actions.setToolbarModal.type: return { ...state, toolbarModal: act.payload }; + case actions.setEditingResource.type: + return { ...state, editingResource: act.payload }; + case actions.setResourceType.type: return { ...state, resourceType: act.payload }; diff --git a/src/types/ResourceEntry.js b/src/types/ResourceEntry.js index 1cc8321f..bbf9b42e 100644 --- a/src/types/ResourceEntry.js +++ b/src/types/ResourceEntry.js @@ -13,6 +13,7 @@ /** * A PHLask resource coming from our backend. * @typedef {Object} ResourceEntry + * @property {string | number | undefined} id The unique identifier for this resource from the database. * @property {number | undefined} version Represents the schema that this resource entry is following. * @property {string} date_created The date this resource was created, in ISO UTC format. * @property {string} creator Who created this resource.