diff --git a/src/components/calculation-history/CalculationHistory.tsx b/src/components/calculation-history/CalculationHistory.tsx new file mode 100644 index 00000000..adf501a3 --- /dev/null +++ b/src/components/calculation-history/CalculationHistory.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { CalculationData } from "../../data/calculations/ICalculationsRepository"; +type CountryDropdownType = { + refreshCalculationHistory: () => void; + calculationHistory: CalculationData[]; +}; +export const CalculationHistory = ({ + refreshCalculationHistory, + calculationHistory, +}: CountryDropdownType) => { + return ( +
+ + +
+ ); +}; diff --git a/src/data/calculations/CalculationsRepository.ts b/src/data/calculations/CalculationsRepository.ts new file mode 100644 index 00000000..2190e2ed --- /dev/null +++ b/src/data/calculations/CalculationsRepository.ts @@ -0,0 +1,66 @@ +import { IStorageRepository } from "../storage/IStorageRepository"; +import { + CalculationData, + ICalculationsRepository, +} from "./ICalculationsRepository"; + +export class CalculationsRepository implements ICalculationsRepository { + remoteDataSource: IStorageRepository = IStorageRepository.instance; + + async storeCalculation(calculationData: CalculationData): Promise { + const oldCalculations = await this.getAllCalculations(); + const newCalculations = [calculationData, ...oldCalculations]; + await this.remoteDataSource.set({ + allCalculations: JSON.stringify(newCalculations), + }); + } + + async cacheOngoingCalculation( + calculationData: CalculationData + ): Promise { + await this.remoteDataSource.set({ + ongoingCalculation: JSON.stringify(calculationData), + }); + } + + async clearOngoingCalculation(): Promise { + await this.remoteDataSource.set({ + ongoingCalculation: null, + }); + } + + async getAllCalculations(): Promise { + const data = await this.remoteDataSource.get({ + allCalculations: JSON.stringify([]), + }); + + return JSON.parse( + data["allCalculations"] as string + ) as CalculationData[]; + } + + async _getOngoingCalculation(): Promise { + const data = await this.remoteDataSource.get({ + ongoingCalculation: null, + }); + + if (data["ongoingCalculation"] !== null) { + return JSON.parse( + data["ongoingCalculation"] as string + ) as CalculationData; + } + return null; + } + + async getLastCalculation(): Promise { + const ongoingCalculation = await this._getOngoingCalculation(); + if (ongoingCalculation !== null) { + return ongoingCalculation; + } + const oldCalculations = await this.getAllCalculations(); + if (oldCalculations.length > 0) { + return oldCalculations[0]; + } + return null; + } +} diff --git a/src/data/calculations/ICalculationsRepository.ts b/src/data/calculations/ICalculationsRepository.ts new file mode 100644 index 00000000..678c07ae --- /dev/null +++ b/src/data/calculations/ICalculationsRepository.ts @@ -0,0 +1,42 @@ +import { CountryName } from "../../constants/Countries"; +import { CalculationsRepository } from "./CalculationsRepository"; +import { TestCalculationsRepository } from "./TestCalculationsRepository"; + +export abstract class ICalculationsRepository { + private static _instance: ICalculationsRepository; + static get instance(): ICalculationsRepository { + if (!this._instance) { + switch (process.env.ENV) { + case "development": + this._instance = new CalculationsRepository(); + break; + case "test": + this._instance = new TestCalculationsRepository(); + break; + default: + throw new Error(`Unknown environment: ${process.env.ENV}`); + } + } + + return this._instance; + } + + abstract storeCalculation(calculationData: CalculationData): Promise; + + abstract cacheOngoingCalculation( + calculationData: CalculationData + ): Promise; + + abstract clearOngoingCalculation(): Promise; + + abstract getLastCalculation(): Promise; + + abstract getAllCalculations(): Promise; +} + +export type CalculationData = { + bytes: number; + emissions: number; + specificEmissions: number; + selectedCountries: Map; +}; diff --git a/src/data/calculations/TestCalculationsRepository.ts b/src/data/calculations/TestCalculationsRepository.ts new file mode 100644 index 00000000..b085f2bc --- /dev/null +++ b/src/data/calculations/TestCalculationsRepository.ts @@ -0,0 +1,38 @@ +import { + CalculationData, + ICalculationsRepository, +} from "./ICalculationsRepository"; + +export class TestCalculationsRepository implements ICalculationsRepository { + private _allCalculations: CalculationData[] = []; + private _ongoingCalculation: CalculationData | null = null; + + async storeCalculation(calculationData: CalculationData): Promise { + const tempArray = [calculationData, ...this._allCalculations]; + this._allCalculations = tempArray; + } + + async cacheOngoingCalculation( + calculationData: CalculationData + ): Promise { + this._ongoingCalculation = calculationData; + } + + async clearOngoingCalculation(): Promise { + this._ongoingCalculation = null; + } + + async getAllCalculations(): Promise { + return this._allCalculations; + } + + async getLastCalculation(): Promise { + if (this._ongoingCalculation !== null) { + return this._ongoingCalculation; + } + if (this._allCalculations.length > 0) { + return this._allCalculations[0]; + } + return null; + } +} diff --git a/src/data/emissions/EmissionsRepository.ts b/src/data/emissions/EmissionsRepository.ts deleted file mode 100644 index 6df39efd..00000000 --- a/src/data/emissions/EmissionsRepository.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IStorageRepository } from "../storage/IStorageRepository"; -import { EmissionsData, IEmissionsRepository } from "./IEmissionsRepository"; - -export class EmissionsRepository implements IEmissionsRepository { - remoteDataSource: IStorageRepository = IStorageRepository.instance; - - async storeLastCalculation(emissionsData: EmissionsData): Promise { - try { - await this.remoteDataSource.set({ - lastCalculation: JSON.stringify(emissionsData), - }); - } catch (e: unknown) { - throw Error(e as string); - } - } - - async getLastCalculation(): Promise { - try { - const data = await this.remoteDataSource.get({ - lastCalculation: JSON.stringify({ - bytes: 0, - emissions: 0, - specificEmissions: 0, - }), - }); - - return JSON.parse( - data["lastCalculation"] as string - ) as EmissionsData; - } catch (e: unknown) { - throw Error(e as string); - } - } -} diff --git a/src/data/emissions/IEmissionsRepository.ts b/src/data/emissions/IEmissionsRepository.ts deleted file mode 100644 index 78641133..00000000 --- a/src/data/emissions/IEmissionsRepository.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { EmissionsRepository } from "./EmissionsRepository"; -import { TestEmissionsRepository } from "./TestEmissionsRepository"; - -export abstract class IEmissionsRepository { - private static _instance: IEmissionsRepository; - static get instance(): IEmissionsRepository { - if (!this._instance) { - switch (process.env.ENV) { - case "development": - this._instance = new EmissionsRepository(); - break; - case "test": - this._instance = new TestEmissionsRepository(); - break; - default: - throw new Error(`Unknown environment: ${process.env.ENV}`); - } - } - - return this._instance; - } - - abstract storeLastCalculation(emissionsData: EmissionsData): Promise; - - abstract getLastCalculation(): Promise; -} - -export type EmissionsData = { - bytes: number; - emissions: number; - specificEmissions: number; -}; diff --git a/src/data/emissions/TestEmissionsRepository.ts b/src/data/emissions/TestEmissionsRepository.ts deleted file mode 100644 index dcdaf823..00000000 --- a/src/data/emissions/TestEmissionsRepository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { EmissionsData, IEmissionsRepository } from "./IEmissionsRepository"; - -export class TestEmissionsRepository implements IEmissionsRepository { - private _lastCalculation: EmissionsData = { - bytes: 0, - emissions: 0, - specificEmissions: 0, - }; - - async storeLastCalculation(emissionsData: EmissionsData): Promise { - this._lastCalculation = emissionsData; - } - - async getLastCalculation(): Promise { - return this._lastCalculation; - } -} diff --git a/src/data/storage/IStorageRepository.ts b/src/data/storage/IStorageRepository.ts index f95dd0f9..4f8757f9 100644 --- a/src/data/storage/IStorageRepository.ts +++ b/src/data/storage/IStorageRepository.ts @@ -1,7 +1,7 @@ import { StorageRepository } from "./StorageRepository"; import { TestStorageRepository } from "./TestStorageRepository"; -export type StorageDataType = string | number; +export type StorageDataType = string | number | null; export abstract class IStorageRepository { private static _instance: IStorageRepository; static get instance(): IStorageRepository { diff --git a/src/popup.tsx b/src/popup.tsx index 92dfed11..c019ff95 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import { CountryDropdown } from "./components/CountryDropdown"; import { usePopup } from "./usePopup"; import { SelectedCountries } from "./components/selected-countries/SelectedCountries"; +import { CalculationHistory } from "./components/calculation-history/CalculationHistory"; export const Popup = () => { const { @@ -15,6 +16,8 @@ export const Popup = () => { averageSpecificEmissions, refreshAndGetSize, stopRecording, + refreshCalculationHistory, + calculationHistory, error, } = usePopup(); @@ -45,6 +48,10 @@ export const Popup = () => { setCountryPercentage={setCountryPercentage} /> + {error &&

{error}

} ); diff --git a/src/usePopup.ts b/src/usePopup.ts index f0b87bec..5630426f 100644 --- a/src/usePopup.ts +++ b/src/usePopup.ts @@ -4,14 +4,17 @@ import { calculateAverageSpecificEmissionsHelper } from "./helpers/calculateAver import { calculateCarbon } from "./helpers/calculateCarbon"; import { ISelectedCountriesRepository } from "./data/selected_countries/ISelectedCountriesRepository"; import { useMountEffect } from "./helpers/useOnceAfterFirstMount"; -import { IEmissionsRepository } from "./data/emissions/IEmissionsRepository"; +import { + CalculationData, + ICalculationsRepository, +} from "./data/calculations/ICalculationsRepository"; import { IBytesRepository } from "./data/bytes/IBytesRepository"; export const usePopup = () => { const selectedCountriesRepository: ISelectedCountriesRepository = ISelectedCountriesRepository.instance; - const emissionsRepository: IEmissionsRepository = - IEmissionsRepository.instance; + const calculationsRepository: ICalculationsRepository = + ICalculationsRepository.instance; const bytesRepository: IBytesRepository = IBytesRepository.instance; const [totalBytesTransferred, settotalBytesTransferred] = useState(0); @@ -21,6 +24,15 @@ export const usePopup = () => { >(new Map()); const [averageSpecificEmissions, setAverageSpecificEmissions] = useState(0); const [error, setError] = useState(); + const [calculationHistory, setCalculationHistory] = useState< + CalculationData[] + >([]); + + const refreshCalculationHistory = async () => { + const calculationsData = + await calculationsRepository.getAllCalculations(); + setCalculationHistory(calculationsData); + }; const setCountryPercentage = async ( country: CountryName, @@ -113,6 +125,19 @@ export const usePopup = () => { } } }); + try { + calculationsRepository.storeCalculation({ + bytes: totalBytesTransferred, + emissions: emissions, + specificEmissions: averageSpecificEmissions, + selectedCountries: selectedCountries, + }); + calculationsRepository.clearOngoingCalculation(); + } catch (e: unknown) { + if (e instanceof Error) { + setError(e.message); + } + } }; const addSelectedCountry = async (country: CountryName) => { @@ -142,10 +167,11 @@ export const usePopup = () => { selectedCountries ); setEmissions(_emissions); - emissionsRepository.storeLastCalculation({ + calculationsRepository.cacheOngoingCalculation({ bytes: changes.totalBytesTransferred.newValue, emissions: _emissions, specificEmissions: averageSpecificEmissions, + selectedCountries: selectedCountries, }); } }; @@ -159,21 +185,28 @@ export const usePopup = () => { totalBytesTransferredListener ); }; - }, [selectedCountries, averageSpecificEmissions, emissionsRepository]); + }, [selectedCountries, averageSpecificEmissions, calculationsRepository]); useMountEffect(() => { - selectedCountriesRepository - .getSelectedCountriesAndPercentages() - .then((newMap) => { - setSelectedCountries(newMap); - }); + const getSelectedCountriesAndSetState = async () => { + const newMap = + await selectedCountriesRepository.getSelectedCountriesAndPercentages(); + setSelectedCountries(newMap); + }; + getSelectedCountriesAndSetState(); }); + useMountEffect(() => { - emissionsRepository.getLastCalculation().then((emissionsData) => { - settotalBytesTransferred(emissionsData.bytes); - setEmissions(emissionsData.emissions); - setAverageSpecificEmissions(emissionsData.specificEmissions); - }); + const getLastCalculationAndSetState = async () => { + const calculationData = + await calculationsRepository.getLastCalculation(); + settotalBytesTransferred(calculationData?.bytes ?? 0); + setEmissions(calculationData?.emissions ?? 0); + setAverageSpecificEmissions( + calculationData?.specificEmissions ?? 0 + ); + }; + getLastCalculationAndSetState(); }); return { @@ -186,6 +219,8 @@ export const usePopup = () => { averageSpecificEmissions, refreshAndGetSize, stopRecording, + calculationHistory, + refreshCalculationHistory, error, }; };