diff --git a/frontend/src/components/common/DropDown/DropDown.tsx b/frontend/src/components/common/DropDown/DropDown.tsx index c24c53fc..799552de 100644 --- a/frontend/src/components/common/DropDown/DropDown.tsx +++ b/frontend/src/components/common/DropDown/DropDown.tsx @@ -7,8 +7,8 @@ import { DropdownWrapper, StyledLabel, StyledSelect } from './dropDown.styles'; interface DropdownProps { label: string; name: string; - value: string; - options: { value: string; label: string }[]; + value: number | string; + options: { value: number; label: string }[]; onChange: (e: React.ChangeEvent) => void; flex?: string; } diff --git a/frontend/src/constants/Constants.ts b/frontend/src/constants/Constants.ts new file mode 100644 index 00000000..aa349091 --- /dev/null +++ b/frontend/src/constants/Constants.ts @@ -0,0 +1,5 @@ +const constants = { + NMP_FILE_KEY: 'nmpFile', +}; + +export default constants; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 0067ab32..d3f8bf1a 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import { SSOProvider } from '@bcgov/citz-imb-sso-react'; import App from './App.tsx'; +import AppProvider from './providers/AppProvider.tsx'; import { env } from '@/env'; createRoot(document.getElementById('root')!).render( @@ -11,9 +12,11 @@ createRoot(document.getElementById('root')!).render( backendURL={env.VITE_BACKEND_URL} idpHint="idir" > - - - + + + + + , ); diff --git a/frontend/src/providers/AppProvider.tsx b/frontend/src/providers/AppProvider.tsx new file mode 100644 index 00000000..f2ab2acd --- /dev/null +++ b/frontend/src/providers/AppProvider.tsx @@ -0,0 +1,27 @@ +/* eslint-disable react-refresh/only-export-components */ +/** + * @summary context provider for app data + */ +import { createContext, ReactNode } from 'react'; +import BaseProvider from './BaseProvider'; +import { initialState, reducer } from '../services/app/AppReducer'; + +export const AppContext = createContext(initialState); + +type AppProviderProps = { + children: ReactNode; +}; + +function AppProvider({ children }: AppProviderProps) { + return ( + + {children} + + ); +} + +export default AppProvider; diff --git a/frontend/src/providers/BaseProvider.tsx b/frontend/src/providers/BaseProvider.tsx new file mode 100644 index 00000000..9a99ab3e --- /dev/null +++ b/frontend/src/providers/BaseProvider.tsx @@ -0,0 +1,33 @@ +/** + * @summary A base context provider to avoid repeating code. + * @param Context The context of the given piece of state, + * this is what gets returned from React.useContext + * @param reducer The reducer for the piece of state. + * @param initialState The initial values for the piece of state. + * @param children The child component of this component + */ +import { Context as ContextType, ReactNode, useReducer, useMemo } from 'react'; + +type BaseProviderProps = { + Context: ContextType; + reducer: (state: StateType, action: any) => StateType; + initialState: StateType; + children: ChildCompsType; +}; + +export default function BaseProvider< + ContextObjType extends Record, + StateType extends Record, + ChildCompsType extends ReactNode, +>(props: BaseProviderProps) { + const { Context, reducer, initialState, children } = props; + + const [state, dispatch] = useReducer(reducer, initialState); + + const contextValue = useMemo(() => ({ state, dispatch }), [state, dispatch]); + return ( + + {children} + + ); +} diff --git a/frontend/src/services/app/AppActions.ts b/frontend/src/services/app/AppActions.ts new file mode 100644 index 00000000..b7ad9da6 --- /dev/null +++ b/frontend/src/services/app/AppActions.ts @@ -0,0 +1,6 @@ +/* eslint-disable no-shadow */ +enum AppActionType { + SET_NMP_FILE = 'SET_NMP_FILE', +} + +export default AppActionType; diff --git a/frontend/src/services/app/AppReducer.ts b/frontend/src/services/app/AppReducer.ts new file mode 100644 index 00000000..404f9a28 --- /dev/null +++ b/frontend/src/services/app/AppReducer.ts @@ -0,0 +1,28 @@ +import AppActionType from './AppActions'; + +const { SET_NMP_FILE } = AppActionType; + +export type AppAction = { + type: AppActionType; + payload?: object; +}; + +// Initial settings state. +export const initialState = { + nmpFile: '', +}; + +/** + * @summary Handles app actions and returns the updated app state. + * @param {object} state - The current app state. + * @param {AppAction} action - The app action to be handled. + * @returns {object} - The updated app state. + */ +export const reducer = (state: object, action: AppAction): object => { + switch (action.type) { + case SET_NMP_FILE: + return { ...state, nmpFile: action.payload }; + default: + throw new Error(); + } +}; diff --git a/frontend/src/services/app/useAppService.ts b/frontend/src/services/app/useAppService.ts new file mode 100644 index 00000000..6cd0bd3b --- /dev/null +++ b/frontend/src/services/app/useAppService.ts @@ -0,0 +1,36 @@ +/* eslint-disable no-console */ +import { useContext, useMemo } from 'react'; +import constants from '../../constants/Constants'; +import { AppContext } from '../../providers/AppProvider'; +import AppActionType from './AppActions'; +import { saveDataToLocalStorage } from '../../utils/AppLocalStorage'; + +const { SET_NMP_FILE } = AppActionType; + +/** + * @summary Custom hook that provides app related functions + */ +const useAppService = () => { + const { state, dispatch } = useContext(AppContext); + + return useMemo(() => { + /** + * @summary Set nmp to local storage and state + */ + const setNMPFile = async (nmpFile: string | ArrayBuffer) => { + try { + saveDataToLocalStorage(constants.NMP_FILE_KEY, nmpFile); + dispatch({ type: SET_NMP_FILE, payload: nmpFile }); + } catch (e) { + console.error(e); + } + }; + + return { + setNMPFile, + state, + }; + }, [state, dispatch]); +}; + +export default useAppService; diff --git a/frontend/src/utils/AppLocalStorage.ts b/frontend/src/utils/AppLocalStorage.ts new file mode 100644 index 00000000..e3588eaa --- /dev/null +++ b/frontend/src/utils/AppLocalStorage.ts @@ -0,0 +1,40 @@ +/** + * @summary Saves data to localStorage + * @param key is the name that the data will be stored by in localStorage + * @param data is the data to be stored + * @type {( key: string, data: any)} + */ +export const saveDataToLocalStorage = (key: string, data: any) => { + try { + localStorage.setItem(key, JSON.stringify(data)); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + } +}; + +/** + * @summary Retrieves data from localStorage if the key exists + * @param key is the key name used to store the value in localStorage + * @type {( key: string )} + * @returns the parsed JSON data or null if the key does not exist in localStorage + */ +export const getDataFromLocalStorage = (key: string) => { + const data = localStorage.getItem(key); + return data ? JSON.parse(data) : null; +}; + +/** + * @summary Checks to see if the key exists in localstorage + * @param key is the key name used to store the value in localStorage + * @type {( key: string )} + * @returns boolean values + */ +export const localStorageKeyExists = (key: string) => getDataFromLocalStorage(key) !== null; + +/** + * @summary Deletes localStorage key + * @param key is the key name used to store the value in localStorage + * @type {( key: string )} + */ +export const deleteLocalStorageKey = (key: string) => localStorage.removeItem(key); diff --git a/frontend/src/views/FarmInformation/FarmInformation.tsx b/frontend/src/views/FarmInformation/FarmInformation.tsx index d6cae7e3..bbced7e2 100644 --- a/frontend/src/views/FarmInformation/FarmInformation.tsx +++ b/frontend/src/views/FarmInformation/FarmInformation.tsx @@ -1,7 +1,9 @@ /** * @summary The Farm Information page for the application */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { localStorageKeyExists } from '../../utils/AppLocalStorage'; +import constants from '../../constants/Constants'; import { ViewContainer, Card, @@ -18,22 +20,44 @@ export default function FarmInformation() { const [formData, setFormData] = useState({ Year: '', FarmName: '', - FarmRegion: '', + FarmRegion: 0, Crops: 'false', HasVegetables: false, HasBerries: false, }); + useEffect(() => { + if (localStorageKeyExists(constants.NMP_FILE_KEY)) { + const data = localStorage.getItem(constants.NMP_FILE_KEY); + if (data) { + try { + const parsedData = JSON.parse(data); + const secondParsedData = JSON.parse(parsedData); + setFormData({ + Year: secondParsedData.farmDetails.Year || '', + FarmName: secondParsedData.farmDetails.FarmName || '', + FarmRegion: secondParsedData.farmDetails.FarmRegion || 0, + Crops: secondParsedData.farmDetails.HasHorticulturalCrops.toString() || 'false', + HasVegetables: secondParsedData.farmDetails.HasVegetables || false, + HasBerries: secondParsedData.farmDetails.HasBerries || false, + }); + } catch (error) { + console.error('Error parsing JSON:', error); + } + } + } + }, []); + const handleChange = (e: React.ChangeEvent) => { const { name, value, type, checked } = e.target as HTMLInputElement; setFormData({ ...formData, [name]: type === 'checkbox' ? checked : value }); }; const regionOptions = [ - { value: '0', label: 'Select a region' }, - { value: '1', label: 'Bulkley-Nechako' }, - { value: '2', label: 'Cariboo' }, - { value: '3', label: 'Columbia Shuswap' }, + { value: 0, label: 'Select a region' }, + { value: 1, label: 'Bulkley-Nechako' }, + { value: 2, label: 'Cariboo' }, + { value: 3, label: 'Columbia Shuswap' }, ]; return ( diff --git a/frontend/src/views/LandingPage/LandingPage.tsx b/frontend/src/views/LandingPage/LandingPage.tsx index 8a58051a..156a5890 100644 --- a/frontend/src/views/LandingPage/LandingPage.tsx +++ b/frontend/src/views/LandingPage/LandingPage.tsx @@ -2,6 +2,9 @@ * @summary The landing page for the application */ import { useNavigate } from 'react-router-dom'; +import constants from '../../constants/Constants'; +import useAppService from '../../services/app/useAppService'; +import { deleteLocalStorageKey } from '../../utils/AppLocalStorage'; import { ButtonWrapper, ViewContainer, @@ -12,6 +15,7 @@ import { import { Button } from '../../components/common'; export default function LandingPage() { + const { setNMPFile } = useAppService(); const navigate = useNavigate(); const handleUpload = () => { @@ -32,17 +36,14 @@ export default function LandingPage() { fr.onload = () => { const data = fr.result; if (data) { - console.log(data.toString()); - // The alert is temporary, will be removed once the data is being used - // eslint-disable-next-line no-alert - alert(data.toString()); + setNMPFile(data); navigate('/farm-information'); } }; }; const newCalcHandler = () => { - localStorage.clear(); + deleteLocalStorageKey(constants.NMP_FILE_KEY); navigate('/farm-information'); };