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 + + +
+

Summary

+ 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 ''; + } +};