diff --git a/package-lock.json b/package-lock.json
index 27dc82e..07dc0fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"next": "12.2.3",
"next-auth": "^4.10.3",
"nodemailer": "^6.7.8",
+ "rc-progress": "^3.4.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.4.0",
@@ -8908,6 +8909,34 @@
"node": ">= 0.8"
}
},
+ "node_modules/rc-progress": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.4.0.tgz",
+ "integrity": "sha512-ZuMyOzzTkZnn+EKqGQ7YHzrvGzBtcCCVjx1McC/E/pMTvr6GWVfVRSawDlWsscxsJs7MkqSTwCO6Lu4IeoY2zQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.6",
+ "rc-util": "^5.16.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/rc-util": {
+ "version": "5.23.0",
+ "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.23.0.tgz",
+ "integrity": "sha512-lgm6diJ/pLgyfoZY59Vz7sW4mSoQCgozqbBye9IJ7/mb5w5h4T7h+i2JpXAx/UBQxscBZe68q0sP7EW+qfkKUg==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "react-is": "^16.12.0",
+ "shallowequal": "^1.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -9411,6 +9440,11 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true
},
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -17135,6 +17169,26 @@
"unpipe": "1.0.0"
}
},
+ "rc-progress": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.4.0.tgz",
+ "integrity": "sha512-ZuMyOzzTkZnn+EKqGQ7YHzrvGzBtcCCVjx1McC/E/pMTvr6GWVfVRSawDlWsscxsJs7MkqSTwCO6Lu4IeoY2zQ==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.6",
+ "rc-util": "^5.16.1"
+ }
+ },
+ "rc-util": {
+ "version": "5.23.0",
+ "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.23.0.tgz",
+ "integrity": "sha512-lgm6diJ/pLgyfoZY59Vz7sW4mSoQCgozqbBye9IJ7/mb5w5h4T7h+i2JpXAx/UBQxscBZe68q0sP7EW+qfkKUg==",
+ "requires": {
+ "@babel/runtime": "^7.18.3",
+ "react-is": "^16.12.0",
+ "shallowequal": "^1.1.0"
+ }
+ },
"react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -17493,6 +17547,11 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true
},
+ "shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/package.json b/package.json
index 7cf40e5..2c78174 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"next": "12.2.3",
"next-auth": "^4.10.3",
"nodemailer": "^6.7.8",
+ "rc-progress": "^3.4.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.4.0",
diff --git a/src/components/Account/Dashboard.tsx b/src/components/Account/Dashboard.tsx
new file mode 100644
index 0000000..47a2046
--- /dev/null
+++ b/src/components/Account/Dashboard.tsx
@@ -0,0 +1,74 @@
+import { useRouter } from 'next/router';
+import React from 'react';
+import { EmissionsSummaryData } from '../../schema/dashboard.schema';
+import { formatWeightToUserUnitPreference } from '../../utils/unitConverter';
+import SummaryTile from './SummaryTile';
+
+interface DashboardProps {
+ greeting: string;
+ emissionsSummaryData: EmissionsSummaryData[] | undefined;
+ unitPreference: string;
+}
+
+const Dashboard = ({
+ greeting,
+ emissionsSummaryData,
+ unitPreference,
+}: DashboardProps) => {
+ const router = useRouter();
+
+ const totalEmissions =
+ emissionsSummaryData?.reduce((previousValue, current) => {
+ return previousValue + current.emissions;
+ }, 0) ?? 0;
+
+ const handleOnClickTile = (type: string) => {
+ switch (type) {
+ case 'Electricity':
+ return router.push('/account/electricity-summary');
+ case 'Driving':
+ return router.push('/account/driving-summary');
+ case 'Fuel':
+ return router.push('/account/fuel-summary');
+ case 'Flight':
+ return router.push('/account/flight-summary');
+ default:
+ return;
+ }
+ };
+
+ if (!emissionsSummaryData) return
Something went wrong
;
+
+ return (
+
+
{greeting}
+
+
Your total emissions to date
+
+ {formatWeightToUserUnitPreference(unitPreference, totalEmissions)}
+
+
+
+
+ To this date, your total emissions break down as follows
+
+
+
+ {emissionsSummaryData.map((classification) => {
+ return (
+ handleOnClickTile(classification.type)}
+ />
+ );
+ })}
+
+
+ );
+};
+
+export default Dashboard;
diff --git a/src/components/Account/ElectricityTable.tsx b/src/components/Account/ElectricityTable.tsx
new file mode 100644
index 0000000..95d3c13
--- /dev/null
+++ b/src/components/Account/ElectricityTable.tsx
@@ -0,0 +1,95 @@
+import { Button, Spinner, Table } from 'flowbite-react';
+import Link from 'next/link';
+import React from 'react';
+import { ElectricityData } from '../../schema/dashboard.schema';
+
+interface ElectricityTableProps {
+ electricityData: ElectricityData[] | undefined;
+}
+
+const ElectricityTable = ({ electricityData }: ElectricityTableProps) => {
+ const electricityDataDatesDesc = () => {
+ const dataToBeSorted = [...electricityData!];
+ if (electricityData)
+ return dataToBeSorted.sort(
+ (a, b) => b.estimated_at.valueOf() - a.estimated_at.valueOf()
+ );
+ };
+
+ if (!electricityData) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Date
+
+ Electricity Used
+
+
+ Emissions
+
+
+
+
Electricity Used
+
Emissions
+
+
+
+
+ {electricityData.length !== 0 ? (
+ electricityDataDatesDesc()!.map((entry) => {
+ return (
+
+
+ {entry.estimated_at.toLocaleDateString()}
+ {entry.estimated_at.toLocaleTimeString()}
+
+ {/** Todo: update this to use the users unit preference */}
+
+ <>
+ {entry.electricity_value} {entry.electricity_unit}
+ >
+
+
+ <>{entry.carbon_g / 1000.0}kg>
+
+
+ <>
+ {entry.electricity_value} {entry.electricity_unit}
+ {entry.carbon_g / 1000.0}kg
+ >
+
+
+ );
+ })
+ ) : (
+
+ )}
+
+
+ {electricityData.length === 0 ? (
+
+
You haven't recorded any electricity data.
+
You can make your emissions calculation here:
+
+
+ ) : (
+ ''
+ )}
+
+ );
+};
+
+export default ElectricityTable;
diff --git a/src/components/Account/FlightTable.tsx b/src/components/Account/FlightTable.tsx
new file mode 100644
index 0000000..466fc35
--- /dev/null
+++ b/src/components/Account/FlightTable.tsx
@@ -0,0 +1,136 @@
+import { Button, Spinner, Table } from 'flowbite-react';
+import Link from 'next/link';
+import React from 'react';
+import { HiArrowRight } from 'react-icons/hi';
+import { FlightData } from '../../schema/dashboard.schema';
+import { FlightLegData } from '../../schema/flight.schema';
+import { trpc } from '../../utils/trpc';
+
+interface FlightTableProps {
+ flightData: FlightData[] | undefined
+}
+const FlightTable = ({flightData}: FlightTableProps) => {
+
+ const flightDataDatesDesc = () => {
+ const dataToBeSorted = [...flightData!];
+ if (flightData)
+ return dataToBeSorted.sort(
+ (a, b) => b.estimated_at.valueOf() - a.estimated_at.valueOf()
+ );
+ };
+
+ if (!flightData) {
+ return (
+
+
+
+ );
+ }
+
+ const getFlightLegs = (flightLegs: FlightLegData[]) => {
+ return (
+
+ {flightLegs.map((leg) => {
+ return (
+
+
+
{leg.cabin_class === 'economy' ? 'Economy:' : 'Premium:'}
{' '}
+
{leg.departure_airport}
+
+
{leg.destination_airport}
+
+
+
+ );
+ })}
+
+ );
+ };
+
+ return (
+
+
+
+ Date
+
+ Passengers
+
+
+ Trip Info
+
+
+ Emissions
+
+
+
+
Trip Summary
+
Emissions
+
+
+
+
+ {flightData.length !== 0 ? (
+ flightDataDatesDesc()!.map((entry) => {
+ return (
+
+
+ {entry.estimated_at.toLocaleDateString()}
+ {entry.estimated_at.toLocaleTimeString()}
+
+
+ {entry.passengers}
+
+ {/** Todo: update this to use the users unit preference */}
+
+ <>
+
+ {entry.passengers}{' '}
+ {entry.passengers > 1 ? 'passengers' : 'passenger'}
+
+ {getFlightLegs(entry.flightLeg)}
+ >
+
+
+ <>{entry.carbon_g / 1000.0}kg>
+
+
+ <>
+
+ {entry.passengers}{' '}
+ {entry.passengers > 1 ? 'passengers' : 'passenger'}
+
+ {entry.flightLeg.length}{' '}
+ {entry.flightLeg.length > 1 ? 'stops' : 'stop'}
+ {entry.carbon_g / 1000.0}kg
+ >
+
+
+ );
+ })
+ ) : (
+
+ )}
+
+
+ {flightData.length === 0 ? (
+
+
You haven't recorded any flight data.
+
You can make your emissions calculation here:
+
+
+ ) : (
+ ''
+ )}
+
+ );
+};
+
+export default FlightTable;
diff --git a/src/components/Account/FuelTable.tsx b/src/components/Account/FuelTable.tsx
new file mode 100644
index 0000000..8ae6c21
--- /dev/null
+++ b/src/components/Account/FuelTable.tsx
@@ -0,0 +1,146 @@
+import { Button, Spinner, Table } from 'flowbite-react';
+import Link from 'next/link';
+import React from 'react';
+import { FuelData } from '../../schema/dashboard.schema';
+
+interface FuelTableProps {
+ fuelData: FuelData[] | undefined;
+}
+const FuelTable = ({ fuelData }: FuelTableProps) => {
+ const electricityDataDatesDesc = () => {
+ const dataToBeSorted = [...fuelData!];
+ if (fuelData)
+ return dataToBeSorted.sort(
+ (a, b) => b.estimated_at.valueOf() - a.estimated_at.valueOf()
+ );
+ };
+
+ const getFormattedFuelData = (fuelUnitType: string, fuelValue: number) => {
+ let formattedFuelUnit = fuelUnitType
+ .split('_')
+ .map((word) => {
+ if (word.includes('btu')) {
+ return word.toUpperCase();
+ }
+
+ return word.charAt(0).toUpperCase() + word.substring(1);
+ })
+ .join(' ');
+
+ if (formattedFuelUnit.includes('Gallon') && fuelValue > 1)
+ formattedFuelUnit += 's';
+
+ return `${fuelValue} ${formattedFuelUnit}`;
+ };
+
+ const getFuelType = (fuelTypeCode: string) => {
+ switch (fuelTypeCode) {
+ case 'ng':
+ return Natural gas
;
+ case 'dfo':
+ return Home Heating and Diesel Fuel
;
+ case 'pg':
+ return Propane Gas
;
+ case 'ker':
+ return Kerosene
;
+ default:
+ return '';
+ }
+ };
+
+ if (!fuelData) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Date
+
+ Type of Fuel Burned
+
+
+ Amount Burned
+
+
+ Emissions
+
+
+
+
+
+
+ {fuelData.length !== 0 ? (
+ electricityDataDatesDesc()!.map((entry) => {
+ return (
+
+
+ {entry.estimated_at.toLocaleDateString()}
+ {entry.estimated_at.toLocaleTimeString()}
+
+
+ {getFuelType(entry.fuel_source_type)}
+
+ {/** Todo: update this to use the users unit preference */}
+
+ <>
+ {getFormattedFuelData(
+ entry.fuel_source_unit,
+ parseFloat(String(entry.fuel_source_value))
+ )}
+ >
+
+
+ <>{entry.carbon_g / 1000.0}kg>
+
+ {/** mobile screen */}
+
+
+
+ {getFuelType(entry.fuel_source_type)}
+
+
+ {getFormattedFuelData(
+ entry.fuel_source_unit,
+ parseFloat(String(entry.fuel_source_value))
+ )}
+
+
{entry.carbon_g / 1000.0}kg
+
+
+
+ );
+ })
+ ) : (
+
+ )}
+
+
+
+ {fuelData.length === 0 ? (
+
+
You haven't recorded any fuel data.
+
You can make your emissions calculation here:
+
+
+ ) : (
+ ''
+ )}
+
+ );
+};
+
+export default FuelTable;
diff --git a/src/components/Account/SummaryTile.tsx b/src/components/Account/SummaryTile.tsx
new file mode 100644
index 0000000..96dc6ae
--- /dev/null
+++ b/src/components/Account/SummaryTile.tsx
@@ -0,0 +1,82 @@
+import { Button, Card } from 'flowbite-react';
+import { Circle } from 'rc-progress';
+import React from 'react';
+import { FaGasPump } from 'react-icons/fa';
+import { ImPowerCord } from 'react-icons/im';
+import { MdDriveEta, MdFlight } from 'react-icons/md';
+import { formatWeightToUserUnitPreference } from '../../utils/unitConverter';
+
+interface SummaryTileProps {
+ type: string;
+ unitPreference: string;
+ emissionsValue: number;
+ totalEmissions: number;
+ handleOnClick: (type: string) => void;
+}
+
+const SummaryTile = ({
+ type,
+ unitPreference,
+ emissionsValue,
+ totalEmissions,
+ handleOnClick,
+}: SummaryTileProps) => {
+ const getEmissionsPercentage = (): string => {
+ if (totalEmissions === 0) {
+ return '0';
+ }
+ const emissionsPercentage = (emissionsValue / totalEmissions) * 100;
+ return emissionsPercentage.toFixed(2);
+ };
+ return (
+
+
+
+
{type}
+ {getIcon(type)}
+
Emissions
+
+ {formatWeightToUserUnitPreference(unitPreference, emissionsValue)}
+
+
+
+
{getEmissionsPercentage()}%
+
+
+
+
+
+
+
+
+ );
+};
+
+const getColor = (emissionsPercentage: number) => {
+ if (emissionsPercentage > 80) return 'red';
+ if (emissionsPercentage > 40) return 'yellow';
+ return 'green';
+};
+
+const getIcon = (type: string) => {
+ const buttonSize = 45;
+
+ switch (type) {
+ case 'Electricity':
+ return ;
+ case 'Fuel':
+ return ;
+ case 'Flight':
+ return ;
+ case 'Driving':
+ return ;
+ default:
+ return;
+ }
+};
+
+export default SummaryTile;
diff --git a/src/components/Account/VehicleTripTable.tsx b/src/components/Account/VehicleTripTable.tsx
new file mode 100644
index 0000000..ff45210
--- /dev/null
+++ b/src/components/Account/VehicleTripTable.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import { Button, Spinner, Table } from 'flowbite-react';
+import Link from 'next/link';
+import { TripData } from '../../schema/dashboard.schema';
+
+interface VehicleTripTableProps {
+ tripData: TripData[];
+}
+
+const VehicleTripTable = ({ tripData }: VehicleTripTableProps) => {
+ const tripDataDatesDesc = () => {
+ const dataToBeSorted = [...tripData!];
+ if (tripData)
+ return dataToBeSorted.sort(
+ (a, b) => b.estimated_at.valueOf() - a.estimated_at.valueOf()
+ );
+ };
+
+ if (!tripData) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Date
+
+ Distance Traveled
+
+
+ Emissions
+
+
+
+
Distance Traveled
+
Emissions
+
+
+
+
+ {tripData.length !== 0 ? (
+ tripDataDatesDesc()!.map((entry) => {
+ return (
+
+
+ {entry.estimated_at.toLocaleDateString()}
+ {entry.estimated_at.toLocaleTimeString()}
+
+ {/** Todo: update this to use the users unit preference */}
+
+ <>
+ {entry.distance_value} {entry.distance_unit}
+ >
+
+
+ <>{entry.carbon_g / 1000.0}kg>
+
+
+ <>
+ {entry.distance_value} {entry.distance_unit}
+ {entry.carbon_g / 1000.0}kg
+ >
+
+
+ );
+ })
+ ) : (
+
+ )}
+
+
+ {tripData.length === 0 ? (
+
+
+ You haven't recorded any driving data for this vehicle.
+
+
You can make your emissions calculation here:
+
+
+ ) : (
+ ''
+ )}
+
+ );
+};
+
+export default VehicleTripTable;
diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx
new file mode 100644
index 0000000..d3f9d31
--- /dev/null
+++ b/src/components/LoadingSpinner.tsx
@@ -0,0 +1,12 @@
+import { Spinner } from 'flowbite-react';
+import React from 'react';
+
+const LoadingSpinner = () => {
+ return (
+
+
+
+ );
+};
+
+export default LoadingSpinner;
diff --git a/src/pages/account/driving-summary.tsx b/src/pages/account/driving-summary.tsx
new file mode 100644
index 0000000..01bf032
--- /dev/null
+++ b/src/pages/account/driving-summary.tsx
@@ -0,0 +1,96 @@
+import { Accordion, Button, Tabs } from 'flowbite-react';
+import Head from 'next/head';
+import Link from 'next/link';
+import React, { ReactElement } from 'react';
+import VehicleTripTable from '../../components/Account/VehicleTripTable';
+import LoadingSpinner from '../../components/LoadingSpinner';
+import AccountLayout from '../../layouts/AccountLayout';
+import { trpc } from '../../utils/trpc';
+import { NextPageWithLayout } from '../_app';
+
+const DrivingSummaryPage: NextPageWithLayout = () => {
+ const { data: vehiclesWithTripsData, isLoading } = trpc.useQuery([
+ 'dashboard.get-vehicle-trip-data',
+ ]);
+
+ if (!vehiclesWithTripsData) {
+ return ;
+ }
+
+ if (vehiclesWithTripsData.length === 0) {
+ return (
+ <>
+
+ Driving Summary
+
+
+
+ Your Driving Emissions
+
+
You haven't recorded any driving data.
+
You can make your emissions calculation here:
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+ Driving Summary
+
+
+
+ Your Driving Emissions
+
+
+ {vehiclesWithTripsData.map((entry) => {
+ return (
+
+
+
+ );
+ })}
+
+
+
+
+ {vehiclesWithTripsData.map((entry) => {
+ return (
+
+
+
+ {[
+ entry.vehicle_year,
+ entry.vehicle_make,
+ entry.vehicle_model,
+ ].join(' ')}
+
+
+
+
+
+
+ );
+ })}
+
+
+ >
+ );
+};
+
+DrivingSummaryPage.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
+
+export default DrivingSummaryPage;
diff --git a/src/pages/account/electricity-summary.tsx b/src/pages/account/electricity-summary.tsx
new file mode 100644
index 0000000..1319fdc
--- /dev/null
+++ b/src/pages/account/electricity-summary.tsx
@@ -0,0 +1,35 @@
+import Head from 'next/head';
+import React, { ReactElement } from 'react';
+import ElectricityTable from '../../components/Account/ElectricityTable';
+import LoadingSpinner from '../../components/LoadingSpinner';
+import AccountLayout from '../../layouts/AccountLayout';
+import { trpc } from '../../utils/trpc';
+import { NextPageWithLayout } from '../_app';
+
+const ElectricitySummaryPage: NextPageWithLayout = () => {
+ const { data: electricityData, isLoading } = trpc.useQuery([
+ 'dashboard.get-electicity-data',
+ ]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
Electricity Summary
+
+
+
+
Your Electricity Emissions
+
+
+ );
+};
+
+ElectricitySummaryPage.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
+
+export default ElectricitySummaryPage;
diff --git a/src/pages/account/flight-summary.tsx b/src/pages/account/flight-summary.tsx
new file mode 100644
index 0000000..4c5391d
--- /dev/null
+++ b/src/pages/account/flight-summary.tsx
@@ -0,0 +1,32 @@
+import Head from 'next/head';
+import React, { ReactElement } from 'react';
+import FlightTable from '../../components/Account/FlightTable';
+import LoadingSpinner from '../../components/LoadingSpinner';
+import AccountLayout from '../../layouts/AccountLayout';
+import { trpc } from '../../utils/trpc';
+import { NextPageWithLayout } from '../_app';
+
+const FlightSummaryPage: NextPageWithLayout = () => {
+ const { data: flightData, isLoading } = trpc.useQuery(['dashboard.get-flight-data']);
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
Flight Summary
+
+
+
+
Your Flight Emissions
+
+
+ );
+};
+
+FlightSummaryPage.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
+export default FlightSummaryPage;
diff --git a/src/pages/account/fuel-summary.tsx b/src/pages/account/fuel-summary.tsx
new file mode 100644
index 0000000..b98f8e7
--- /dev/null
+++ b/src/pages/account/fuel-summary.tsx
@@ -0,0 +1,33 @@
+import Head from 'next/head';
+import React, { ReactElement } from 'react';
+import FuelTable from '../../components/Account/FuelTable';
+import LoadingSpinner from '../../components/LoadingSpinner';
+import AccountLayout from '../../layouts/AccountLayout';
+import { trpc } from '../../utils/trpc';
+import { NextPageWithLayout } from '../_app';
+
+const FuelSummaryPage: NextPageWithLayout = () => {
+ const { data: fuelData, isLoading } = trpc.useQuery(['dashboard.get-fuel-data']);
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
Fuel Summary
+
+
+
+
Your Fuel Emissions
+
+
+ );
+};
+
+FuelSummaryPage.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
+
+export default FuelSummaryPage;
diff --git a/src/pages/account/index.tsx b/src/pages/account/index.tsx
index 75e2bba..c99dc73 100644
--- a/src/pages/account/index.tsx
+++ b/src/pages/account/index.tsx
@@ -1,38 +1,54 @@
-import { router } from '@trpc/server';
-import { Button, Spinner } from 'flowbite-react';
-import { signOut, useSession } from 'next-auth/react';
+import { useSession } from 'next-auth/react';
+import Head from 'next/head';
import { useRouter } from 'next/router';
-import { useEffect, useState } from 'react';
+import { ReactElement, useEffect } from 'react';
+import Dashboard from '../../components/Account/Dashboard';
+import LoadingSpinner from '../../components/LoadingSpinner';
+import AccountLayout from '../../layouts/AccountLayout';
+import { trpc } from '../../utils/trpc';
+import { NextPageWithLayout } from '../_app';
-export default function AccountPage() {
+const AccountPage: NextPageWithLayout = () => {
const { data: session } = useSession();
const router = useRouter();
+ const greetingMessage = `Welcome, ${session?.user?.name}`;
+
+ const { data: emissionsSummaryData, isLoading: isEmissionsLoading } =
+ trpc.useQuery(['dashboard.summary']);
+
+ const { data: userPreferences, isLoading: isPreferencesLoading } =
+ trpc.useQuery(['preferences.get-preferences']);
useEffect(() => {
if (!session) {
- router.push('/');
+ router.push('/auth/login');
}
}, [session]);
if (!session) {
- return (
-
- Access denied. Please sign in.
-
- )
+ return Access denied. Please sign in.
;
}
+
+ if (isEmissionsLoading || isPreferencesLoading) return ;
+
return (
-
-
Account Page
-
-
-
+ <>
+
+ Dashboard
+
+
+
+
+ >
);
-}
+};
+
+AccountPage.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
+export default AccountPage;
diff --git a/src/schema/dashboard.schema.ts b/src/schema/dashboard.schema.ts
new file mode 100644
index 0000000..3bd9244
--- /dev/null
+++ b/src/schema/dashboard.schema.ts
@@ -0,0 +1,52 @@
+import { Prisma } from '@prisma/client';
+import z from 'zod';
+
+export const summarySchema = z.array(
+ z.object({ type: z.string(), emissions: z.number() })
+);
+
+export type EmissionsSummaryData = {
+ type: string;
+ emissions: number;
+};
+
+export type ElectricityData = {
+ electricity_value: Prisma.Decimal;
+ electricity_unit: string;
+ carbon_g: number;
+ estimated_at: Date;
+ id: string;
+};
+
+export type FlightData = {
+ id: string;
+ carbon_g: number;
+ estimated_at: Date;
+ passengers: number;
+ distance_unit: string;
+ distance_value: Prisma.Decimal;
+ flightLeg: {
+ id: string;
+ departure_airport: string;
+ destination_airport: string;
+ cabin_class: string;
+ legNumber: number;
+ }[];
+};
+
+export type FuelData = {
+ id: string;
+ carbon_g: number;
+ estimated_at: Date;
+ fuel_source_type: string;
+ fuel_source_unit: string;
+ fuel_source_value: Prisma.Decimal;
+};
+
+export type TripData = {
+ carbon_g: number;
+ estimated_at: Date;
+ distance_unit: string;
+ distance_value: Prisma.Decimal;
+ id: string;
+};
diff --git a/src/schema/flight.schema.ts b/src/schema/flight.schema.ts
index e0379e2..94c9951 100644
--- a/src/schema/flight.schema.ts
+++ b/src/schema/flight.schema.ts
@@ -14,12 +14,21 @@ const flightLegSchema = z.object({
const distanceUnitSchema = z.enum(['km', 'mi']);
+const flightLegDataSchema = z.object({
+ departure_airport: z.string(),
+ destination_airport: z.string(),
+ legNumber: z.number(),
+ id: z.string(),
+ cabin_class: z.string()
+})
+
export const flightRequestSchema = z.object({
passengers: z.number(),
distance_unit: distanceUnitSchema,
legs: z.array(flightLegSchema),
});
+export type FlightLegData = z.TypeOf
export type CabinClass = z.TypeOf;
export type FlightLeg = z.TypeOf;
diff --git a/src/server/router/dashboard.router.ts b/src/server/router/dashboard.router.ts
new file mode 100644
index 0000000..b2e90e6
--- /dev/null
+++ b/src/server/router/dashboard.router.ts
@@ -0,0 +1,190 @@
+import { summarySchema } from '../../schema/dashboard.schema';
+import { createRouter } from './context';
+
+export const dashboardRouter = createRouter()
+ .query('summary', {
+ output: summarySchema,
+ async resolve({ ctx }) {
+ const user = ctx.session?.user;
+
+ if (user) {
+ const electricityCarbon = await ctx.prisma.electricityUse
+ .aggregate({
+ _sum: {
+ carbon_g: true,
+ },
+ where: {
+ userId: user.id,
+ },
+ })
+ .then((response) => response._sum.carbon_g);
+
+ const vehicleCarbon = await ctx.prisma.vehicle
+ .findMany({
+ select: {
+ trips: {
+ select: {
+ carbon_g: true,
+ },
+ },
+ },
+ where: {
+ userId: user.id,
+ },
+ })
+ .then((response) =>
+ response.reduce((previous, current) => {
+ return (
+ previous +
+ current.trips.reduce((previous, current) => {
+ return previous + current.carbon_g;
+ }, 0)
+ );
+ }, 0)
+ );
+
+ const fuelCarbon = await ctx.prisma.fuelUsed
+ .aggregate({
+ _sum: {
+ carbon_g: true,
+ },
+ where: {
+ userId: user.id,
+ },
+ })
+ .then((response) => response._sum.carbon_g);
+
+ const flightCarbon = await ctx.prisma.flight
+ .aggregate({
+ _sum: {
+ carbon_g: true,
+ },
+ where: {
+ userId: user.id,
+ },
+ })
+ .then((response) => response._sum.carbon_g);
+
+ return [
+ { type: 'Electricity', emissions: electricityCarbon ?? 0 },
+ { type: 'Driving', emissions: vehicleCarbon ?? 0 },
+ { type: 'Fuel', emissions: fuelCarbon ?? 0 },
+ { type: 'Flight', emissions: flightCarbon ?? 0 },
+ ];
+ }
+
+ return [];
+ },
+ })
+ .query('get-fuel-data', {
+ async resolve({ ctx }) {
+ const user = ctx.session?.user;
+
+ if (user) {
+ const fuelRecords = await ctx.prisma.fuelUsed
+ .findMany({
+ select: {
+ id: true,
+ fuel_source_type: true,
+ fuel_source_unit: true,
+ fuel_source_value: true,
+ carbon_g: true,
+ estimated_at: true,
+ },
+ where: {
+ userId: user.id,
+ },
+ })
+ .then((response) => response);
+
+ return fuelRecords;
+ }
+ },
+ })
+ .query('get-electicity-data', {
+ async resolve({ ctx }) {
+ const user = ctx.session?.user;
+
+ if (user) {
+ const electricityRecords = await ctx.prisma.electricityUse
+ .findMany({
+ select: {
+ id: true,
+ electricity_unit: true,
+ electricity_value: true,
+ carbon_g: true,
+ estimated_at: true,
+ },
+ where: {
+ userId: user.id,
+ },
+ })
+ .then((response) => response);
+
+ return electricityRecords;
+ }
+ },
+ })
+ .query('get-flight-data', {
+ async resolve({ ctx }) {
+ const user = ctx.session?.user;
+
+ if (user) {
+ const flightRecords = await ctx.prisma.flight
+ .findMany({
+ select: {
+ id: true,
+ passengers: true,
+ distance_value: true,
+ distance_unit: true,
+ carbon_g: true,
+ estimated_at: true,
+ flightLeg: {
+ select: {
+ id: true,
+ departure_airport: true,
+ destination_airport: true,
+ cabin_class: true,
+ legNumber: true,
+ },
+ },
+ },
+ where: {
+ userId: user.id,
+ },
+ })
+ .then((response) => response);
+
+ return flightRecords;
+ }
+ },
+ })
+ .query('get-vehicle-trip-data', {
+ async resolve({ ctx }) {
+ const user = ctx.session?.user;
+
+ if (user) {
+ const vehicleTripRecords = await ctx.prisma.vehicle
+ .findMany({
+ select: {
+ id: true,
+ vehicle_make: true,
+ vehicle_model: true,
+ vehicle_year: true,
+ trips: {
+ select: {
+ id: true,
+ distance_value: true,
+ distance_unit: true,
+ carbon_g: true,
+ estimated_at: true,
+ },
+ },
+ },
+ })
+ .then((response) => response);
+
+ return vehicleTripRecords;
+ }
+ },
+ });
diff --git a/src/server/router/index.ts b/src/server/router/index.ts
index 6acd40c..321d13a 100644
--- a/src/server/router/index.ts
+++ b/src/server/router/index.ts
@@ -1,6 +1,5 @@
// src/server/router/index.ts
import { createRouter } from "./context";
-import superjson from "superjson";
import { exampleRouter } from './example';
import { protectedExampleRouter } from './protected-example-router';
@@ -9,6 +8,7 @@ import { flightRouter } from "./flight.router";
import { vehicleRouter } from './vehicle.router';
import { fuelRouter } from './fuel.router';
import { preferencesRouter } from "./preferences.router";
+import { dashboardRouter } from "./dashboard.router";
export const appRouter = createRouter()
.merge('example.', exampleRouter)
@@ -17,6 +17,7 @@ export const appRouter = createRouter()
.merge('flight.', flightRouter)
.merge('vehicle.', vehicleRouter)
.merge('fuel.', fuelRouter)
+ .merge('dashboard.', dashboardRouter)
.merge('preferences.', preferencesRouter);
// export type definition of API
diff --git a/src/utils/unitConverter.ts b/src/utils/unitConverter.ts
new file mode 100644
index 0000000..68d6720
--- /dev/null
+++ b/src/utils/unitConverter.ts
@@ -0,0 +1,14 @@
+export const formatWeightToUserUnitPreference = (
+ unitPreference: string,
+ value: number
+): string => {
+ switch (unitPreference) {
+ case 'metric':
+ return `${value / 1000.0} kg`;
+ case 'imperial':
+ const lbs = (value / 1000.0) * 2.2
+ return `${lbs.toFixed(2)} lbs`;
+ default:
+ return '';
+ }
+};