Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [NMP-79] Frontend State Management #92

Merged
merged 17 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)) {
PaulGarewal marked this conversation as resolved.
Show resolved Hide resolved
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
Loading