diff --git a/src/components/BathroomResourceForm/BathroomResourceForm.tsx b/src/components/BathroomResourceForm/BathroomResourceForm.tsx index a611031d..08b8bcde 100644 --- a/src/components/BathroomResourceForm/BathroomResourceForm.tsx +++ b/src/components/BathroomResourceForm/BathroomResourceForm.tsx @@ -18,6 +18,7 @@ type BathroomResourceFormProps = { defaultValues?: ResourceEntry | null; onSubmit: (values: FormValues) => void; onGoBack?: VoidFunction; + onClose?: VoidFunction; isSubmitting?: boolean; }; @@ -30,13 +31,15 @@ const BathroomResourceForm = ({ defaultValues, isSubmitting, onSubmit, - onGoBack + onGoBack, + onClose: onCloseProp }: BathroomResourceFormProps) => { const { setToolbarModal } = useToolbarContext(); const onClose = () => { if (onGoBack) { onGoBack(); } + onCloseProp?.(); setToolbarModal(null); }; diff --git a/src/components/EditResourceForm/EditResourceForm.tsx b/src/components/EditResourceForm/EditResourceForm.tsx index 3e4e147d..a013463e 100644 --- a/src/components/EditResourceForm/EditResourceForm.tsx +++ b/src/components/EditResourceForm/EditResourceForm.tsx @@ -48,29 +48,37 @@ const EditResourceForm = () => { const resourceForms = { [ResourceType.WATER]: ( setResourceEditCandidate(null)} onSubmit={onSubmit} /> ), [ResourceType.FOOD]: ( setResourceEditCandidate(null)} onSubmit={onSubmit} /> ), [ResourceType.FORAGE]: ( setResourceEditCandidate(null)} onSubmit={onSubmit} /> ), [ResourceType.BATHROOM]: ( setResourceEditCandidate(null)} onSubmit={onSubmit} /> ) diff --git a/src/components/FoodResourceForm/FoodResourceForm.tsx b/src/components/FoodResourceForm/FoodResourceForm.tsx index 167c36c8..292604ff 100644 --- a/src/components/FoodResourceForm/FoodResourceForm.tsx +++ b/src/components/FoodResourceForm/FoodResourceForm.tsx @@ -26,6 +26,7 @@ type FoodResourceFormProps = { defaultValues?: ResourceEntry | null; onSubmit: (values: FormValues) => void; onGoBack?: VoidFunction; + onClose?: VoidFunction; isSubmitting?: boolean; }; @@ -36,6 +37,7 @@ const FoodResourceForm = ({ defaultValues = null, isSubmitting = false, onGoBack, + onClose: onCloseProp, onSubmit }: FoodResourceFormProps) => { const { setToolbarModal } = useToolbarContext(); @@ -43,6 +45,7 @@ const FoodResourceForm = ({ if (onGoBack) { onGoBack(); } + onCloseProp?.(); setToolbarModal(null); }; diff --git a/src/components/ForageResourceForm/ForageResourceForm.tsx b/src/components/ForageResourceForm/ForageResourceForm.tsx index 87a94f84..80ecccfa 100644 --- a/src/components/ForageResourceForm/ForageResourceForm.tsx +++ b/src/components/ForageResourceForm/ForageResourceForm.tsx @@ -20,6 +20,7 @@ type ForageResourceFormProps = { defaultValues?: ResourceEntry | null; onSubmit: (values: FormValues) => void; onGoBack?: VoidFunction; + onClose?: VoidFunction; isSubmitting?: boolean; }; @@ -30,13 +31,15 @@ const ForageResourceForm = ({ defaultValues, isSubmitting, onSubmit, - onGoBack + onGoBack, + onClose: onCloseProp }: ForageResourceFormProps) => { const { setToolbarModal } = useToolbarContext(); const onClose = () => { if (onGoBack) { onGoBack(); } + onCloseProp?.(); setToolbarModal(null); }; diff --git a/src/components/WaterResourceForm/WaterResourceForm.tsx b/src/components/WaterResourceForm/WaterResourceForm.tsx index a1b44821..13614bff 100644 --- a/src/components/WaterResourceForm/WaterResourceForm.tsx +++ b/src/components/WaterResourceForm/WaterResourceForm.tsx @@ -21,6 +21,7 @@ type WaterResourceFormProps = { defaultValues?: ResourceEntry | null; onSubmit: (values: FormValues) => void; onGoBack?: VoidFunction; + onClose?: VoidFunction; isSubmitting?: boolean; }; @@ -31,6 +32,7 @@ const WaterResourceForm = ({ defaultValues = null, isSubmitting = false, onGoBack, + onClose: onCloseProp, onSubmit }: WaterResourceFormProps) => { const { setToolbarModal } = useToolbarContext(); @@ -38,6 +40,7 @@ const WaterResourceForm = ({ if (onGoBack) { onGoBack(); } + onCloseProp?.(); setToolbarModal(null); }; diff --git a/src/components/forms/FormAddressField/FormResourceAddressField.tsx b/src/components/forms/FormAddressField/FormResourceAddressField.tsx index d0679d6c..2279152e 100644 --- a/src/components/forms/FormAddressField/FormResourceAddressField.tsx +++ b/src/components/forms/FormAddressField/FormResourceAddressField.tsx @@ -1,10 +1,13 @@ -import { Autocomplete, TextField } from '@mui/material'; +import { Autocomplete, Box, TextField } from '@mui/material'; import { toLatLngLiteral } from '@vis.gl/react-google-maps'; import useGooglePlacesAutocomplete from 'hooks/useGooglePlacesAutocomplete'; import useAddressFormControllers from 'hooks/useAddressFormControllers'; import UseMyLocationButton from 'components/UseMyLocationButton/UseMyLocationButton'; import getNormalizedAddressComponents from 'utils/getNormalizedAddressComponents'; +const LOCATION_NOT_FOUND_ERROR = + "We couldn't find your location, please select an option from the search"; + type FormResourceAddressFieldProps = { label?: string; fullWidth?: boolean; @@ -14,10 +17,16 @@ const FormResourceAddressField = ({ label = 'Address', fullWidth = false }: FormResourceAddressFieldProps) => { - const { suggestions, isFetching, onChange, onDebouncedChange } = + const { suggestions, isFetching, onDebouncedChange } = useGooglePlacesAutocomplete(); - const { inputRef, error, onClear, setAddressError, setAddressValues } = - useAddressFormControllers(); + const { + inputRef, + addressValue, + error, + onClear, + setAddressError, + setAddressValues + } = useAddressFormControllers(); const onSelect = async (place: google.maps.places.Place) => { const googlePlacesId = place.id; @@ -66,30 +75,36 @@ const FormResourceAddressField = ({ const { places } = await google.maps.places.Place.searchNearby({ locationRestriction: circle, - fields: ['formattedAddress'] + fields: ['id', 'formattedAddress'] }); const firstPlace = places.at(0); if (!firstPlace?.formattedAddress) { - return setAddressError( - "We couldn't find your location, please select an option from the search" - ); + return setAddressError(LOCATION_NOT_FOUND_ERROR); } - onChange(firstPlace.formattedAddress); + await onSelect(firstPlace); }; return ( - openOnFocus options={suggestions} fullWidth={fullWidth} - onInputChange={(_event, value) => { + value={addressValue || null} + onInputChange={(_event, value, reason) => { + if (reason !== 'input') { + return; + } onDebouncedChange(value); }} loading={isFetching} - getOptionKey={option => option.placeId} - getOptionLabel={option => option.text.text} + getOptionKey={option => + typeof option === 'string' ? option : option.placeId + } + getOptionLabel={option => + typeof option === 'string' ? option : option.text.text + } onChange={(_event, value, reason) => { if (reason === 'clear') { return onClear(); @@ -99,7 +114,7 @@ const FormResourceAddressField = ({ return; } - if (!value) { + if (!value || typeof value === 'string') { return; } @@ -120,12 +135,19 @@ const FormResourceAddressField = ({ inputRef={inputRef} error={Boolean(error)} helperText={ - error?.message || ( - - ) + <> + {error?.message && ( + + {error.message} + + )} + {error?.message !== LOCATION_NOT_FOUND_ERROR && ( + + )} + } /> )} diff --git a/src/hooks/useAddressFormControllers.ts b/src/hooks/useAddressFormControllers.ts index 16f512b3..ff8446a4 100644 --- a/src/hooks/useAddressFormControllers.ts +++ b/src/hooks/useAddressFormControllers.ts @@ -10,7 +10,7 @@ type ResourceAddressField = | 'zip_code'; const useAddressFormControllers = () => { - const { control, setError, resetField } = useFormContext(); + const { control, setError } = useFormContext(); const addressController = useController({ name: 'address', control }); const cityController = useController({ name: 'city', control }); const stateController = useController({ name: 'state', control }); @@ -39,12 +39,22 @@ const useAddressFormControllers = () => { }; const onClear = () => { - controllers.forEach(controller => resetField(controller.field.name)); + setAddressValues({ + address: '', + city: '', + state: '', + gp_id: '', + latitude: Number.NaN, + longitude: Number.NaN, + zip_code: '' + }); }; return { inputRef: addressController.field.ref, - error: addressController.fieldState.error, + addressValue: addressController.field.value, + error: + addressController.fieldState.error ?? latitudeController.fieldState.error, setAddressValues, setAddressError, onClear diff --git a/src/schemas/baseResourceSchema.ts b/src/schemas/baseResourceSchema.ts index aec852b2..7cce2bb2 100644 --- a/src/schemas/baseResourceSchema.ts +++ b/src/schemas/baseResourceSchema.ts @@ -5,12 +5,16 @@ const baseResourceSchema = z.object({ version: z.number().optional().default(1), name: z.string().nonempty('Name is required').default(''), address: z.string().nonempty('Address is required').default(''), - gp_id: z.string().nonempty('Google Places ID is required').default(''), + gp_id: z.string().default(''), city: z.string().default(''), state: z.string().default(''), zip_code: z.string().default(''), - latitude: z.number().default(Number.NaN), - longitude: z.number().default(Number.NaN), + latitude: z + .number({ message: 'Please select an address from the list' }) + .default(Number.NaN), + longitude: z + .number({ message: 'Please select an address from the list' }) + .default(Number.NaN), date_created: z.iso.datetime().default(() => new Date().toISOString()), last_modifier: z.string().default('phlask_app'), last_modified: z.iso.datetime().default(() => new Date().toISOString()), @@ -30,7 +34,7 @@ const baseResourceSchema = z.object({ verification: z .object({ verified: z.boolean().default(false), - last_modified: z.iso.datetime().default(() => new Date().toISOString()), + last_modified: z.iso.datetime().catch(() => new Date().toISOString()), verifier: z.string().default('') }) .default(() => ({ @@ -38,7 +42,10 @@ const baseResourceSchema = z.object({ last_modified: new Date().toISOString(), verifier: '' })), - images: z.array(z.string()).default([]), + images: z + .array(z.string().nullable()) + .default([]) + .transform(images => images.filter(image => image !== null)), guidelines: z.string().default(''), resource_type: z.enum( [ @@ -50,7 +57,13 @@ const baseResourceSchema = z.object({ { message: 'Resource type is required' } ), status: z - .enum(['OPERATIONAL', 'TEMPORARILY_CLOSED', 'PERMANENTLY_CLOSED', 'HIDDEN']) + .enum([ + 'OPERATIONAL', + 'TEMPORARILY_CLOSED', + 'PERMANENTLY_CLOSED', + 'HIDDEN', + 'NONOPERATIONAL' + ]) .default('OPERATIONAL') }); diff --git a/src/schemas/bathroomResourceSchema.ts b/src/schemas/bathroomResourceSchema.ts index bd2739e7..8cf29825 100644 --- a/src/schemas/bathroomResourceSchema.ts +++ b/src/schemas/bathroomResourceSchema.ts @@ -8,16 +8,18 @@ const bathroomResourceSchema = baseResourceSchema.extend({ .default(ResourceType.BATHROOM), bathroom: z .object({ - tags: z.array( - z.enum([ - 'WHEELCHAIR_ACCESSIBLE', - 'GENDER_NEUTRAL', - 'CHANGING_TABLE', - 'SINGLE_OCCUPANCY', - 'HAS_FOUNTAIN', - 'FAMILY' - ]) - ) + tags: z + .array( + z.enum([ + 'WHEELCHAIR_ACCESSIBLE', + 'GENDER_NEUTRAL', + 'CHANGING_TABLE', + 'SINGLE_OCCUPANCY', + 'HAS_FOUNTAIN', + 'FAMILY' + ]) + ) + .default([]) }) .default({ tags: [] }) }); diff --git a/src/schemas/foodResourceSchema.ts b/src/schemas/foodResourceSchema.ts index f5ef655c..0dbed972 100644 --- a/src/schemas/foodResourceSchema.ts +++ b/src/schemas/foodResourceSchema.ts @@ -6,9 +6,13 @@ const foodResourceSchema = baseResourceSchema.extend({ resource_type: z.literal(ResourceType.FOOD).default(ResourceType.FOOD), food: z .object({ - food_type: z.array(z.enum(['PERISHABLE', 'NON_PERISHABLE', 'PREPARED'])), - distribution_type: z.array(z.enum(['EAT_ON_SITE', 'DELIVERY', 'PICKUP'])), - organization_name: z.string(), + food_type: z + .array(z.enum(['PERISHABLE', 'NON_PERISHABLE', 'PREPARED'])) + .default([]), + distribution_type: z + .array(z.enum(['EAT_ON_SITE', 'DELIVERY', 'PICKUP'])) + .default([]), + organization_name: z.string().default(''), organization_url: z .union([ z @@ -20,13 +24,10 @@ const foodResourceSchema = baseResourceSchema.extend({ z.literal('') ]) .default(''), - organization_type: z.enum([ - 'GOVERNMENT', - 'BUSINESS', - 'NON_PROFIT', - 'UNSURE' - ]), - tags: z.array(z.string()) + organization_type: z + .enum(['GOVERNMENT', 'BUSINESS', 'NON_PROFIT', 'UNSURE']) + .default('UNSURE'), + tags: z.array(z.string()).default([]) }) .default({ food_type: [], diff --git a/src/schemas/foragingResourceSchema.ts b/src/schemas/foragingResourceSchema.ts index ba62704a..c424a209 100644 --- a/src/schemas/foragingResourceSchema.ts +++ b/src/schemas/foragingResourceSchema.ts @@ -6,10 +6,12 @@ const foragingResourceSchema = baseResourceSchema.extend({ resource_type: z.literal(ResourceType.FORAGE).default(ResourceType.FORAGE), forage: z .object({ - forage_type: z.array( - z.enum(['NUT', 'FRUIT', 'LEAVES', 'BARK', 'FLOWERS']) - ), - tags: z.array(z.enum(['MEDICINAL', 'IN_SEASON', 'COMMUNITY_GARDEN'])) + forage_type: z + .array(z.enum(['NUT', 'FRUIT', 'LEAVES', 'BARK', 'FLOWERS'])) + .default([]), + tags: z + .array(z.enum(['MEDICINAL', 'IN_SEASON', 'COMMUNITY_GARDEN'])) + .default([]) }) .default({ forage_type: [], tags: [] }) }); diff --git a/src/services/db.ts b/src/services/db.ts index 24898b62..da79da78 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -1,5 +1,9 @@ import { createClient } from '@supabase/supabase-js'; -import type { Provider, ResourceEntry } from 'types/ResourceEntry'; +import type { + Provider, + ResourceEntry, + ResourceStatus +} from 'types/ResourceEntry'; import type { ResourceTypeOption } from 'hooks/useResourceType'; import type { Contributor } from 'types/Contributor'; import type { FeedbackForm } from 'types/FeedbackEntry'; @@ -25,11 +29,7 @@ export type FetchResourcesOptions = { /** Filter by resource type */ resourceType?: ResourceTypeOption; /** Filter by status */ - status?: - | 'OPERATIONAL' - | 'TEMPORARILY_CLOSED' - | 'PERMANENTLY_CLOSED' - | 'HIDDEN'; + status?: ResourceStatus; filters?: { name: string; value: string | string[] }[]; }; @@ -128,7 +128,7 @@ export const addResourceEdit = async (values: ResourceEdit) => { throw error; } return data; -} +}; export const addFeedback = async (feedback: FeedbackForm) => { const { data, error } = await supabase diff --git a/src/types/ResourceEntry.ts b/src/types/ResourceEntry.ts index e65d22c8..5cc3db59 100644 --- a/src/types/ResourceEntry.ts +++ b/src/types/ResourceEntry.ts @@ -121,7 +121,8 @@ export type ResourceStatus = | 'OPERATIONAL' | 'TEMPORARILY_CLOSED' | 'PERMANENTLY_CLOSED' - | 'HIDDEN'; + | 'HIDDEN' + | 'NONOPERATIONAL'; /** An organization or entity that provided/contributed this resource. */ export type Provider = {