Skip to content

Commit

Permalink
feat: [NMP-79] Frontend State Management (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
dallascrichmond authored Dec 10, 2024
1 parent 6c655e4 commit 472690b
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 16 deletions.
4 changes: 2 additions & 2 deletions frontend/src/components/common/DropDown/DropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLSelectElement>) => void;
flex?: string;
}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/constants/Constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const constants = {
NMP_FILE_KEY: 'nmpFile',
};

export default constants;
9 changes: 6 additions & 3 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -11,9 +12,11 @@ createRoot(document.getElementById('root')!).render(
backendURL={env.VITE_BACKEND_URL}
idpHint="idir"
>
<BrowserRouter>
<App />
</BrowserRouter>
<AppProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</AppProvider>
</SSOProvider>
</StrictMode>,
);
27 changes: 27 additions & 0 deletions frontend/src/providers/AppProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<BaseProvider
Context={AppContext}
initialState={initialState}
reducer={reducer}
>
{children}
</BaseProvider>
);
}

export default AppProvider;
33 changes: 33 additions & 0 deletions frontend/src/providers/BaseProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<ContextObjType, StateType, ChildCompsType> = {
Context: ContextType<ContextObjType>;
reducer: (state: StateType, action: any) => StateType;
initialState: StateType;
children: ChildCompsType;
};

export default function BaseProvider<
ContextObjType extends Record<string, any>,
StateType extends Record<string, any>,
ChildCompsType extends ReactNode,
>(props: BaseProviderProps<ContextObjType, StateType, ChildCompsType>) {
const { Context, reducer, initialState, children } = props;

const [state, dispatch] = useReducer(reducer, initialState);

const contextValue = useMemo(() => ({ state, dispatch }), [state, dispatch]);
return (
<Context.Provider value={contextValue as unknown as ContextObjType}>
{children}
</Context.Provider>
);
}
6 changes: 6 additions & 0 deletions frontend/src/services/app/AppActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-disable no-shadow */
enum AppActionType {
SET_NMP_FILE = 'SET_NMP_FILE',
}

export default AppActionType;
28 changes: 28 additions & 0 deletions frontend/src/services/app/AppReducer.ts
Original file line number Diff line number Diff line change
@@ -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();
}
};
36 changes: 36 additions & 0 deletions frontend/src/services/app/useAppService.ts
Original file line number Diff line number Diff line change
@@ -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<any>(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;
40 changes: 40 additions & 0 deletions frontend/src/utils/AppLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -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);
36 changes: 30 additions & 6 deletions frontend/src/views/FarmInformation/FarmInformation.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<HTMLInputElement | HTMLSelectElement>) => {
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 (
Expand Down
11 changes: 6 additions & 5 deletions frontend/src/views/LandingPage/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -12,6 +15,7 @@ import {
import { Button } from '../../components/common';

export default function LandingPage() {
const { setNMPFile } = useAppService();
const navigate = useNavigate();

const handleUpload = () => {
Expand All @@ -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');
};

Expand Down

0 comments on commit 472690b

Please sign in to comment.