diff --git a/package-lock.json b/package-lock.json index 5651587..a5fa74f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", + "@fontsource/lato": "^5.0.20", "@fontsource/roboto": "^4.5.7", "@mui/material": "^5.8.7", "@react-pdf/renderer": "^3.3.8", @@ -24,6 +25,7 @@ "@visx/text": "^2.10.0", "@visx/tooltip": "^2.10.0", "@visx/visx": "^3.1.2", + "animate.css": "^4.1.1", "concurrently": "^8.0.1", "cross-env": "^7.0.3", "electron-log": "^4.4.8", @@ -34,6 +36,7 @@ "react-dom": "^18.2.0", "react-plotly.js": "^2.6.0", "react-scripts": "5.0.1", + "swiper": "^11.0.7", "typescript": "^4.7.4", "wait-on": "^7.0.1", "web-vitals": "^2.1.4" @@ -2563,6 +2566,11 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fontsource/lato": { + "version": "5.0.20", + "resolved": "https://registry.npmjs.org/@fontsource/lato/-/lato-5.0.20.tgz", + "integrity": "sha512-2ej7KDuTFoea6Q2hWjx3Png1+MdNcW4V6l7sw/vNauuxCv9xBIZCmpXnTz9eVdj/5Ui//jiWWiQk57mGwjCNwA==" + }, "node_modules/@fontsource/roboto": { "version": "4.5.8", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz", @@ -9810,6 +9818,11 @@ "resolved": "https://registry.npmjs.org/almost-equal/-/almost-equal-1.1.0.tgz", "integrity": "sha512-0V/PkoculFl5+0Lp47JoxUcO0xSxhIBvm+BxHdD/OgXNmdRpRHCFnKVuUoWyS9EzQP+otSGv0m9Lb4yVkQBn2A==" }, + "node_modules/animate.css": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", + "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==" + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -32781,6 +32794,24 @@ "node": ">=4.0.0" } }, + "node_modules/swiper": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.0.7.tgz", + "integrity": "sha512-cDfglW1B6uSmB6eB6pNmzDTNLmZtu5bWWa1vak0RU7fOI9qHjMzl7gVBvYSl34b0RU2N11HxxETJqQ5LeqI1cA==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -36311,6 +36342,11 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.35.0.tgz", "integrity": "sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==" }, + "@fontsource/lato": { + "version": "5.0.20", + "resolved": "https://registry.npmjs.org/@fontsource/lato/-/lato-5.0.20.tgz", + "integrity": "sha512-2ej7KDuTFoea6Q2hWjx3Png1+MdNcW4V6l7sw/vNauuxCv9xBIZCmpXnTz9eVdj/5Ui//jiWWiQk57mGwjCNwA==" + }, "@fontsource/roboto": { "version": "4.5.8", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz", @@ -42123,6 +42159,11 @@ "resolved": "https://registry.npmjs.org/almost-equal/-/almost-equal-1.1.0.tgz", "integrity": "sha512-0V/PkoculFl5+0Lp47JoxUcO0xSxhIBvm+BxHdD/OgXNmdRpRHCFnKVuUoWyS9EzQP+otSGv0m9Lb4yVkQBn2A==" }, + "animate.css": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", + "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==" + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -59798,6 +59839,11 @@ "util.promisify": "~1.0.0" } }, + "swiper": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.0.7.tgz", + "integrity": "sha512-cDfglW1B6uSmB6eB6pNmzDTNLmZtu5bWWa1vak0RU7fOI9qHjMzl7gVBvYSl34b0RU2N11HxxETJqQ5LeqI1cA==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index 63e9c26..1936a8b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dependencies": { "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", + "@fontsource/lato": "^5.0.20", "@fontsource/roboto": "^4.5.7", "@mui/material": "^5.8.7", "@react-pdf/renderer": "^3.3.8", @@ -23,6 +24,7 @@ "@visx/text": "^2.10.0", "@visx/tooltip": "^2.10.0", "@visx/visx": "^3.1.2", + "animate.css": "^4.1.1", "concurrently": "^8.0.1", "cross-env": "^7.0.3", "electron-log": "^4.4.8", @@ -33,6 +35,7 @@ "react-dom": "^18.2.0", "react-plotly.js": "^2.6.0", "react-scripts": "5.0.1", + "swiper": "^11.0.7", "typescript": "^4.7.4", "wait-on": "^7.0.1", "web-vitals": "^2.1.4" diff --git a/src/App.scss b/src/App.scss index b6b59a9..3f224a8 100644 --- a/src/App.scss +++ b/src/App.scss @@ -103,4 +103,263 @@ $landfill-gasses: #f06807; #dashboardText { font-weight: bold; +} +// EOG slider + +.swiper { + width: 100%; + height: 100%; +} + +.swiper-slide { + text-align: center; + font-size: 18px; + background: #fff; + display: flex; + justify-content: center; + align-items: center; + + &::before { + content: ''; + //todo problem with animations? + @extend .slide1-image; + } +} + +.swiper-slide img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.autoplay-progress { + position: absolute; + right: 16px; + bottom: 16px; + z-index: 10; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + color: var(--swiper-theme-color); + } + + .autoplay-progress svg { + --progress: 0; + position: absolute; + left: 0; + top: 0px; + z-index: 10; + width: 100%; + height: 100%; + stroke-width: 4px; + stroke: var(--swiper-theme-color); + fill: none; + stroke-dashoffset: calc(125.6 * (1 - var(--progress))); + stroke-dasharray: 125.6; + transform: rotate(-90deg); + } + + // Animations + +// #animate-underline-emphasis { +// display: inline-block; +// position: relative; +// padding-bottom: 3px; +// } + +// @keyframes border { +// from { +// width: 0%; +// } + +// to { +// width: 100%; +// } +// } + +// #animate-underline-emphasis:after { +// content: ''; +// display: block; +// margin: auto; +// height: 5px; +// width: 0px; +// background: #fff; +// animation-name: border; +// animation-duration: 1000ms; +// animation-delay: 2s; +// animation-fill-mode: both; +// } + +// todo replace w above +.animate-underline-emphasis { + border-bottom: 5px solid #fff; + padding-bottom: 3px; + margin: auto; + height: 5px; + padding-bottom: 0px; +} + +// @keyframes fade-color { +// 0% { color: rgba(68,68,68,.6); } +// 100% { color: white; } +// } + + +// .animate-color { +// color: rgba(68,68,68,.6); +// padding-left: 12px; +// padding-right: 12px; +// font-weight: 1000; +// font-size: 36px; +// animation: fade-color 3s; +// animation-delay: 1s; +// animation-fill-mode: both; +// } + +@keyframes fade-opacity { + 0% { opacity: 1; } + 100% { opacity: 0; } +} + + +#fade-to-white { + &::before { + position: absolute; + background-color: #000; + display: flex; + // width: 100%; + height: 100%; + // animation: fade-opacity 10s; + // animation-delay: 6s; + // animation-fill-mode: forwards; + } +} + +// Slides +.swiper-slide.swiper-slide-active.lose-game-bg { + &::before { + position: fixed; + z-index: -1; + background: url('images/lose-image-opt.jpg'); + // filter: blur($blur-radius); + background-size: cover; + top: -1 * $blur-radius; + left: -1 * $blur-radius; + bottom: -1 * $blur-radius; + right: -1 * $blur-radius; + display: block; + } +} + +.swiper-slide.swiper-slide-active.slide1-image { + &::before { + position: fixed; + z-index: -1; + background: url('images/turbine2-opt.jpg') ; + // filter: blur($blur-radius); + background-size: cover; + top: -1 * $blur-radius; + left: -1 * $blur-radius; + bottom: -1 * $blur-radius; + right: -1 * $blur-radius; + display: block; + } +} + +.swiper-slide.swiper-slide-active.slide2-image { + &::before { + position: fixed !important; + z-index: -1; + background: url('images/solar-array2-opt.jpg') !important; + background-size: cover !important; + + // filter: blur($blur-radius); + top: -1 * $blur-radius; + left: -1 * $blur-radius; + bottom: -1 * $blur-radius; + right: -1 * $blur-radius; + display: block; + } +} + +.swiper-slide.swiper-slide-active.slide3-image { + &::before { + position: fixed !important; + z-index: -1; + background: url('images/electricity.jpg') !important; + background-size: cover !important; + + // filter: blur($blur-radius); + top: -1 * $blur-radius; + left: -1 * $blur-radius; + bottom: -1 * $blur-radius; + right: -1 * $blur-radius; + display: block; + } + +} + +.swiper-slide.swiper-slide-active.slide4-image { + &::before { + position: fixed !important; + z-index: -1; + background: url('images/field-opt.jpg') !important; + background-size: cover !important; + + // filter: blur($blur-radius); + top: -1 * $blur-radius; + left: -1 * $blur-radius; + bottom: -1 * $blur-radius; + right: -1 * $blur-radius; + display: block; + } +} + +// .lose-game-text { +// display: 'flex'; +// flex-direction: 'column'; +// flex-grow: 1; +// color: #000; +// border-radius: 10px; +// padding: 36px 128px; +// animation-fill-mode: 'forwards'; +// opacity: 0; +// font-size: 32px; +// font-weight: 900; +// text-transform: uppercase; +// font-family:"Lato", sans-serif; +// } + +.lose-game-text { + display: 'flex'; + flex-direction: 'column'; + flex-grow: 1; + color: #000; + border-radius: 10px; + padding: 36px 64px; + animation-fill-mode: 'forwards'; + opacity: 0; + font-size: 36px; + letter-spacing: 0px; + line-height: 3rem; + font-weight: 900; + text-transform: uppercase; + font-family:"Lato", sans-serif; +} + +.slide-stat-div { + font-size: 42px; + color: #fff; + font-weight: 900; + border-radius: 10px; + letter-spacing: 1.5px; + text-align: center; + text-transform: uppercase; + font-family: "Lato", sans-serif; + line-height: 4rem; + margin-top: 0; } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 30dfc21..f74ae19 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,9 +6,13 @@ import '@fontsource/roboto/400.css'; import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; +import '@fontsource/lato/400.css'; +import '@fontsource/lato/700.css'; +import '@fontsource/lato/900.css'; + import type { ControlCallbacks } from './components/controls'; -import { calculateEmissions } from './trackedStats'; -import type { TrackedStats, YearCostSavings } from './trackedStats'; +import { calculateEmissions, setCostPerCarbonSavings } from './trackedStats'; +import type { EndGameResults, TrackedStats, YearCostSavings } from './trackedStats'; import { updateStatsGaugeMaxValues } from './trackedStats'; import { getYearCostSavings } from './trackedStats'; import { initialTrackedStats, setCarbonEmissionsAndSavings } from './trackedStats'; @@ -16,12 +20,12 @@ import { Dashboard } from './components/Dashboard'; import Pages, { PageError } from './Pages'; import { PageControls } from './PageControls'; import { Scope1Projects, Scope2Projects } from './ProjectControl'; -import type { CostSavings, ImplementedProject, RenewableProject} from './ProjectControl'; +import type { CostSavings, ImplementedProject, ProjectControl, RenewableProject} from './ProjectControl'; import type { CompletedProject, SelectedProject} from './ProjectControl'; import { resolveToValue, cloneAndModify, rightArrow } from './functions-and-types'; import { theme } from './components/theme'; import { closeDialogButton } from './components/Buttons'; -import { YearRecap, getHasActiveHiddenCost } from './components/YearRecap'; +import { ImplementedFinancingData, YearRecap, getHasActiveHiddenCost } from './components/YearRecap'; import ScopeTabs from './components/ScopeTabs'; import { CurrentPage } from './components/CurrentPage'; import { InfoDialog, InfoDialogControlProps, InfoDialogStateProps, fillInfoDialogProps, getDefaultWarningDialogProps, getEmptyInfoDialogState } from './components/Dialogs/InfoDialog'; @@ -29,7 +33,9 @@ import { CompareDialog } from './components/Dialogs/CompareDialog'; import { ProjectDialog, ProjectDialogControlProps, ProjectDialogStateProps, fillProjectDialogProps, getEmptyProjectDialog } from './components/Dialogs/ProjectDialog'; import Projects from './Projects'; import { GameSettings, UserSettings, getYearlyBudget } from './components/SelectGameSettings'; -import { CapitalFundingState, FinancingOption, findFinancingOptionFromProject, getCanUseCapitalFunding, isProjectFullyFunded, resetCapitalFundingState, setCapitalFundingMilestone } from './Financing'; +import { CapitalFundingState, FinancingOption, findFinancingOptionFromProject, getCanUseCapitalFunding, getProjectedFinancedSpending, isProjectFullyFunded, resetCapitalFundingState, setCapitalFundingMilestone } from './Financing'; +import WinGame from './components/WinGame'; +import LoseGame from './components/LoseGame'; export type AppState = { @@ -80,6 +86,7 @@ export type AppState = { gameSettings: GameSettings; defaultTrackedStats : TrackedStats; yearlyCostSavings: YearCostSavings[]; + endGameResults?: EndGameResults; } // JL note: I could try and do some fancy TS magic to make all the AppState whatsits optional, but @@ -107,6 +114,7 @@ export interface NextAppState { snackbarOpen?: boolean; snackbarContent?: JSX.Element; isCompareDialogOpen?: boolean; + endGameResults: EndGameResults; } export class App extends React.PureComponent { @@ -221,8 +229,7 @@ export class App extends React.PureComponent { if (componentClass === InfoDialog) { infoDialog = fillInfoDialogProps(controlProps); infoDialog.isOpen = true; - } - else { + } else { // * If navigating back to project menu or other from a dialog, close dialog infoDialog = cloneAndModify(this.state.infoDialog, { isOpen: false }); projectDialog = cloneAndModify(this.state.projectDialog, {isOpen: false}); @@ -586,16 +593,16 @@ export class App extends React.PureComponent { trackedStats: newYearTrackedStats, yearRangeInitialStats: newYearRangeInitialStats, capitalFundingState: newCapitalFundingState, - yearlyCostSavings: yearlyCostSavings + yearlyCostSavings: yearlyCostSavings, }; const isGameWon = newYearTrackedStats.carbonSavingsPercent >= 0.5; - const isEndOfGame = newYearTrackedStats.currentGameYear === this.state.gameSettings.totalGameYears + 1 - + const isEndOfGame = newYearTrackedStats.currentGameYear === this.state.gameSettings.totalGameYears + 1; + if (isGameWon) { - this.endGame(Pages.winScreen, newYearTrackedStats, nextState); + this.endGame(true, newYearTrackedStats, nextState); } else if (isEndOfGame) { - this.endGame(Pages.loseScreen, newYearTrackedStats, nextState); + this.endGame(false, newYearTrackedStats, nextState); } else { this.setState(nextState); this.setPage(Pages.scope1Projects); @@ -603,12 +610,66 @@ export class App extends React.PureComponent { } - endGame(page: symbol, newYearTrackedStats: TrackedStats, nextState) { + // todo 279 - NextAppState vs AppState conflicts, whats the intent with these? + endGame(isWinningGame: boolean, newYearTrackedStats: TrackedStats, nextState) { // Game is over - don't advance year newYearTrackedStats.currentGameYear -= 1; nextState.trackedStats = newYearTrackedStats; + + const endYearStats: TrackedStats = nextState.trackedStats + const completedProjects: CompletedProject[] = nextState.completedProjects; + const implementedRenewableProjects: ImplementedProject[] = nextState.implementedRenewableProjects + const implementedFinancedProjects: ImplementedProject[] = nextState.implementedFinancedProjects + let completedRenewables = nextState.implementedRenewableProjects.map(renewable => { + return { + completedYear: endYearStats.currentGameYear - 1, + gameYearsImplemented: renewable.gameYearsImplemented, + yearStarted: renewable.yearStarted, + financingOption: renewable.financingOption, + page: renewable.page + } + }); + + const allCompletedProjects = completedProjects.concat(completedRenewables); + + // * sub year to get projections + endYearStats.currentGameYear - 1; + let projectedFinancedSpending = getProjectedFinancedSpending( + implementedFinancedProjects, + implementedRenewableProjects, + endYearStats); + let gameCurrentAndProjectedSpending = endYearStats.gameTotalSpending + projectedFinancedSpending; + setCarbonEmissionsAndSavings(endYearStats, this.state.defaultTrackedStats); + setCostPerCarbonSavings(endYearStats, gameCurrentAndProjectedSpending); + + const noDecimalsFormatter = Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }); + const carbonSavingsPercentFormatted: string = (endYearStats.carbonSavingsPercent * 100).toFixed(2); + const gameTotalNetCostFormatted: string = noDecimalsFormatter.format(endYearStats.gameTotalSpending); + const projectedFinancedSpendingFormatted: string = noDecimalsFormatter.format(projectedFinancedSpending); + const gameCurrentAndProjectedSpendingFormatted: string = noDecimalsFormatter.format(projectedFinancedSpending + endYearStats.gameTotalSpending); + const costPerCarbonSavingsFormatted: string = endYearStats.costPerCarbonSavings !== undefined ? Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }).format(endYearStats.costPerCarbonSavings) : '0'; + + // todo add NAN / undefined defensives + let endGameResults: EndGameResults = { + carbonSavingsPercent: carbonSavingsPercentFormatted, + gameTotalSpending: gameTotalNetCostFormatted, + projectedFinancedSpending: projectedFinancedSpendingFormatted, + gameCurrentAndProjectedSpending: gameCurrentAndProjectedSpendingFormatted, + costPerCarbonSavings: costPerCarbonSavingsFormatted, + completedProjects: allCompletedProjects, + endYearStats: endYearStats, + isWinningGame: isWinningGame + } + + nextState.endGameResults = endGameResults; this.setState(nextState); - this.setPage(page); + this.setPage(Pages.endGameDialog); } setNewYearStats(newYearTrackedStats: TrackedStats, newBudget: number, currentYearStats: TrackedStats) { @@ -846,6 +907,7 @@ export class App extends React.PureComponent { yearRangeInitialStats={this.state.yearRangeInitialStats} handleGameSettingsOnProceed={(userSettings) => this.handleGameSettingsOnProceed(userSettings)} handleNewYearSetupOnProceed={(yearFinalStats, capitalFundingState) => this.setupNewYearOnProceed(yearFinalStats, capitalFundingState)} + endGameResults={this.state.endGameResults} /> : <>} diff --git a/src/PageControls.tsx b/src/PageControls.tsx index 5572ec1..9994765 100644 --- a/src/PageControls.tsx +++ b/src/PageControls.tsx @@ -18,6 +18,8 @@ import { newAppPageDialogControl } from './components/Dialogs/InfoDialog'; import Projects from './Projects'; import { newYearRecapControl } from './components/YearRecap'; import { newEndGameReportPageControl } from './components/EndGameReport/EndGameReportPage'; +import { newWinGameControl } from './components/WinGame'; +import { newEndGameDialogControl } from './components/Dialogs/EndGameDialog'; declare interface PageControls { [key: symbol]: PageControl; } @@ -56,57 +58,6 @@ PageControls[Pages.introduction] = newAppPageDialogControl({ PageControls[Pages.selectGameSettings] = newSelectGameSettingsControl({}); -PageControls[Pages.winScreen] = newAppPageDialogControl({ - title: 'CONGRATULATIONS!', - text: (state) => `You succeeded at the goal! \n You managed to decarbonize {${state.companyName}} by {${(state.trackedStats.carbonSavingsPercent * 100).toFixed(1)}%} in 10 years or less! \n You reduced CO2e Emissions by a total of {${state.trackedStats.carbonSavingsPerKg.toLocaleString(undefined, { maximumFractionDigits: 0 })} kg CO2e}! \n You saved a total of {$${state.trackedStats.costPerCarbonSavings.toFixed(2)}/kg CO2e}! \n You spent a total of {$${state.trackedStats.yearEndTotalSpending.toLocaleString()}} and completed {${state.completedProjects.length}} projects!`, - img: 'images/confetti.png', - buttons: [ - { - text: 'View Report', - variant: 'text', - size: 'large', - onClick: function () { - return Pages.endGameReport; - } - }, - { - text: 'Play again', - variant: 'text', - size: 'large', - onClick: (state) => { - location.href = String(location.href); // Reload the page - - return state.currentPage; // The page returned doesn't really matter - } - } - ] -}); - -PageControls[Pages.loseScreen] = newAppPageDialogControl({ - title: 'Sorry...', - text: (state) => `Sorry, looks like you didn't succeed at decarbonizing {${state.companyName}} by 50%. You got to {${(state.trackedStats.carbonSavingsPercent * 100).toFixed(1)}%} in 10 years. Try again?`, - buttons: [ - { - text: 'View Report', - variant: 'text', - size: 'large', - onClick: function () { - return Pages.endGameReport; - } - }, - { - text: 'Try again', - variant: 'text', - onClick: (state) => { - location.href = String(location.href); // Reload the page - - return state.currentPage; // The page returned doesn't really matter - } - } - ] -}); - - PageControls[Pages.selectScope] = newGroupedChoicesControl({ title: function (state, nextState) { // Year 1 @@ -243,6 +194,8 @@ PageControls[Pages.scope2Projects] = newGroupedChoicesControl({ hideDashboard: false, }, Pages.selectScope); + +PageControls[Pages.endGameDialog] = newEndGameDialogControl(); PageControls[Pages.yearRecap] = newYearRecapControl(Pages.selectScope); PageControls[Pages.endGameReport] = newEndGameReportPageControl(); diff --git a/src/Pages.tsx b/src/Pages.tsx index 3417dc8..9217ba8 100644 --- a/src/Pages.tsx +++ b/src/Pages.tsx @@ -13,8 +13,9 @@ const Pages = { scope1Projects: Symbol('scope1Projects'), scope2Projects: Symbol('scope2Projects'), yearRecap: Symbol('year-recap'), - winScreen: Symbol('win-screen'), - loseScreen: Symbol('lose-screen'), + winGame: Symbol('win-game'), + loseGame: Symbol('lose-game'), + endGameDialog: Symbol('end-game-dialog'), endGameReport: Symbol('end-game-report'), // below: scope 1 projects wasteHeatRecovery: Symbol('waste-heat-recovery'), diff --git a/src/components/Buttons.tsx b/src/components/Buttons.tsx index 51bf8fb..17f1b1d 100644 --- a/src/components/Buttons.tsx +++ b/src/components/Buttons.tsx @@ -56,7 +56,6 @@ export declare interface ButtonGroupProps extends ControlCallbacks { * Whether the entire group of buttons appears disabled. */ disabled?: Resolvable; - doPageCallback: (callback?: PageCallback) => void; /** * Whether to use a MUI Stack component to space the buttons, or just include the array of buttons "raw". * @default true diff --git a/src/components/CurrentPage.tsx b/src/components/CurrentPage.tsx index 46b95ac..4cb3db2 100644 --- a/src/components/CurrentPage.tsx +++ b/src/components/CurrentPage.tsx @@ -2,7 +2,7 @@ import React, { Fragment } from 'react'; import type { ImplementedProject, RenewableProject} from '../ProjectControl'; import type { CompletedProject, SelectedProject} from '../ProjectControl'; import { PureComponentIgnoreFuncs } from '../functions-and-types'; -import type { TrackedStats } from '../trackedStats'; +import type { EndGameResults, TrackedStats } from '../trackedStats'; import { GroupedChoices } from './GroupedChoices'; import type { GroupedChoicesProps } from './GroupedChoices'; import { GameSettings, SelectGameSettings, UserSettings } from './SelectGameSettings'; @@ -12,6 +12,8 @@ import { YearRecap } from './YearRecap'; import type { PageControlProps, ControlCallbacks } from './controls'; import { CapitalFundingState } from '../Financing'; import EndGameReportPage from './EndGameReport/EndGameReportPage'; +import EndGameDialog from './Dialogs/EndGameDialog'; +import EndGameReport from './EndGameReport/EndGameReportPage'; interface CurrentPageProps extends ControlCallbacks, PageControlProps { @@ -28,6 +30,7 @@ interface CurrentPageProps extends ControlCallbacks, PageControlProps { yearRangeInitialStats: TrackedStats[]; gameSettings: GameSettings; defaultTrackedStats :TrackedStats; + endGameResults: EndGameResults; handleGameSettingsOnProceed: (userSettings: UserSettings) => void; handleNewYearSetupOnProceed: (yearFinalStats: TrackedStats, capitalFundingState: CapitalFundingState) => void; } @@ -85,19 +88,23 @@ export class CurrentPage extends PureComponentIgnoreFuncs { yearRangeInitialStats={this.props.yearRangeInitialStats} handleNewYearSetup={this.props.handleNewYearSetupOnProceed} />; - case EndGameReportPage: + case EndGameDialog: return ( - + + ); + case EndGameReport: + return ( + ); default: return <>; diff --git a/src/components/Dialogs/EndGameDialog.tsx b/src/components/Dialogs/EndGameDialog.tsx new file mode 100644 index 0000000..4656502 --- /dev/null +++ b/src/components/Dialogs/EndGameDialog.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Dialog, DialogActions } from '@mui/material'; +import { ControlCallbacks, PageControl } from '../controls'; +import { EndGameResults, TrackedStats } from '../../trackedStats'; +import { GameSettings } from '../SelectGameSettings'; +import WinGame from '../WinGame'; +import Pages from '../../Pages'; +import { ButtonGroup, ButtonGroupButton } from '../Buttons'; +import LoseGame from '../LoseGame'; + + +export default function EndGameDialog(props: EndGameDialogProps) { + const buttons: ButtonGroupButton[] = [{ + text: 'View Report', + variant: 'text', + size: 'large', + onClick: function () { + return Pages.endGameReport; + } + }, + { + text: 'New Game', + variant: 'text', + size: 'large', + onClick: (state) => { + location.href = String(location.href); + return state.currentPage; + } + }]; + + return ( + + {props.endGameResults.isWinningGame && } + {!props.endGameResults.isWinningGame && } + + + + + + ); +} + +export interface EndGameDialogProps extends + ControlCallbacks, + GameSettings { + yearRangeInitialStats: TrackedStats[]; + endGameResults: EndGameResults; +} + +export function newEndGameDialogControl(): PageControl { + return { + componentClass: EndGameDialog, + controlProps: {}, + hideDashboard: true, + }; +} \ No newline at end of file diff --git a/src/components/EndGameReport/EndGameReportPage.tsx b/src/components/EndGameReport/EndGameReportPage.tsx index 55a650d..985f30e 100644 --- a/src/components/EndGameReport/EndGameReportPage.tsx +++ b/src/components/EndGameReport/EndGameReportPage.tsx @@ -1,70 +1,39 @@ import React, { Fragment } from 'react'; -import { TrackedStats, setCarbonEmissionsAndSavings, setCostPerCarbonSavings } from '../../trackedStats'; +import { EndGameResults, TrackedStats } from '../../trackedStats'; import { GameSettings } from '../SelectGameSettings'; -import { CapitalFundingState, FinancingOption, getIsAnnuallyFinanced, getProjectedFinancedSpending, isProjectFullyFunded } from '../../Financing'; +import { FinancingOption, isProjectFullyFunded } from '../../Financing'; import { ControlCallbacks, Emphasis, PageControl } from '../controls'; -import { Box, Button, Card, CardContent, CardHeader, Grid, Link, List, ListItem, ListItemIcon, ListItemText, Tooltip, TooltipProps, Typography, styled, tooltipClasses } from '@mui/material'; +import { Box, Card, CardContent, CardHeader, Grid, Link, List, ListItem, ListItemIcon, ListItemText, Typography } from '@mui/material'; import { ParentSize } from '@visx/responsive'; -import { CompletedProject, ImplementedProject, ProjectControl, RenewableProject } from '../../ProjectControl'; +import { ProjectControl } from '../../ProjectControl'; import { ImplementedFinancingData } from '../YearRecap'; import Projects from '../../Projects'; import { DialogFinancingOptionCard } from '../Dialogs/ProjectDialog'; -import { parseSpecialText, truncate } from '../../functions-and-types'; +import { parseSpecialText } from '../../functions-and-types'; import EnergyUseLineChart from '../EnergyUseLineChart'; -import DownloadPDF, { ReportPDFProps } from './DownloadPDF'; +import DownloadPDF from './DownloadPDF'; import InfoIcon from '@mui/icons-material/Info'; -export default class EndGameReportPage extends React.Component { - render() { - const yearRangeInitialStats: TrackedStats[] = [...this.props.yearRangeInitialStats]; - const endYearStats: TrackedStats = { ...this.props.trackedStats} - let completedRenewables = [...this.props.implementedRenewableProjects].map(renewable => { - return { - completedYear: endYearStats.currentGameYear - 1, - gameYearsImplemented: renewable.gameYearsImplemented, - yearStarted: renewable.yearStarted, - financingOption: renewable.financingOption, - page: renewable.page - } - }); - - let completedProjects = this.props.completedProjects.concat(completedRenewables); - return ( - - - - ); - } - -} - -function EndGameReport(props: ReportProps) { +export default function EndGameReport(props: ReportProps) { let projectRecapCards: JSX.Element[] = []; - props.completedProjects.forEach(project => { + props.endGameResults.completedProjects.forEach(project => { let implementedProject: ProjectControl = Projects[project.page]; let implementationFinancing: FinancingOption = project.financingOption; let isFinanced = implementationFinancing.financingType.id !== 'budget'; let financingData: ImplementedFinancingData = { option: implementationFinancing, - isPaidOff: isFinanced ? isProjectFullyFunded(project, props.endYearStats) : false, + isPaidOff: isFinanced ? isProjectFullyFunded(project, props.endGameResults.endYearStats) : false, isFinanced: isFinanced, } - let projectNetCost = implementedProject.getYearEndTotalSpending(financingData.option, props.endYearStats.gameYearInterval); + let projectNetCost = implementedProject.getYearEndTotalSpending(financingData.option, props.endGameResults.endYearStats.gameYearInterval); let totalProjectExtraCosts = implementedProject.getHiddenCost(); projectRecapCards.push( getProjectCard( implementedProject, - props.endYearStats, + props.endGameResults.endYearStats, projectNetCost, totalProjectExtraCosts, financingData, @@ -72,33 +41,9 @@ function EndGameReport(props: ReportProps) { }); - // * sub year to get projections - props.endYearStats.currentGameYear - 1; - let projectedFinancedSpending = getProjectedFinancedSpending(props.implementedFinancedProjects, props.renewableProjects, props.endYearStats); - let gameCurrentAndProjectedSpending = props.endYearStats.gameTotalSpending + projectedFinancedSpending; - setCarbonEmissionsAndSavings(props.endYearStats, props.defaultTrackedStats); - setCostPerCarbonSavings(props.endYearStats, gameCurrentAndProjectedSpending); - let endOfGameResults = { - carbonSavingsPercent: props.endYearStats.carbonSavingsPercent, - gameTotalSpending: props.endYearStats.gameTotalSpending, - projectedFinancedSpending: projectedFinancedSpending, - gameCurrentAndProjectedSpending: gameCurrentAndProjectedSpending, - costPerCarbonSavings: props.endYearStats.costPerCarbonSavings - } - const noDecimalsFormatter = Intl.NumberFormat('en-US', { - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }); - const carbonSavingsPercentFormatted: string = (endOfGameResults.carbonSavingsPercent * 100).toFixed(2); - const gameTotalNetCostFormatted: string = noDecimalsFormatter.format(endOfGameResults.gameTotalSpending); - const projectedFinancedSpendingFormatted: string = noDecimalsFormatter.format(endOfGameResults.projectedFinancedSpending); - const costPerCarbonSavingsFormatted: string = endOfGameResults.costPerCarbonSavings !== undefined ? Intl.NumberFormat('en-US', { - minimumFractionDigits: 0, - maximumFractionDigits: 2, - }).format(endOfGameResults.costPerCarbonSavings) : '0'; - return ( - + + {(parent) => ( Your company has reduced CO2e Emissions by{' '} - {carbonSavingsPercentFormatted}%{' '} + {props.endGameResults.carbonSavingsPercent}%{' '} } /> @@ -129,7 +74,7 @@ function EndGameReport(props: ReportProps) { - You have spent{' '}${gameTotalNetCostFormatted}{' '} throughout the game. + You have spent{' '}${props.endGameResults.gameTotalSpending}{' '} throughout the game. } /> @@ -141,12 +86,12 @@ function EndGameReport(props: ReportProps) { - Your cost per kg reduced was{' '}${costPerCarbonSavingsFormatted}/kg CO2e{' '} + Your cost per kg reduced was{' '}${props.endGameResults.costPerCarbonSavings}/kg CO2e{' '} } /> - {endOfGameResults.projectedFinancedSpending > 0 && + {props.endGameResults.projectedFinancedSpending && @@ -154,7 +99,7 @@ function EndGameReport(props: ReportProps) { - You are projected to spend {' '}${projectedFinancedSpendingFormatted}{' '} on financed and renewed projects in future years + You are projected to spend {' '}${props.endGameResults.projectedFinancedSpending}{' '} on financed and renewed projects in future years } /> @@ -166,7 +111,7 @@ function EndGameReport(props: ReportProps) { {projectRecapCards.length > 0 && @@ -187,15 +132,13 @@ function EndGameReport(props: ReportProps) { } - + ); } -interface ReportProps extends ReportPDFProps { - endYearStats: TrackedStats, - defaultTrackedStats: TrackedStats, - implementedFinancedProjects: ImplementedProject[], - renewableProjects: RenewableProject[] +interface ReportProps { + endGameResults: EndGameResults, + yearRangeInitialStats: TrackedStats[]; } @@ -407,7 +350,7 @@ function getProjectCard(implementedProject: ProjectControl, export function newEndGameReportPageControl(): PageControl { return { - componentClass: EndGameReportPage, + componentClass: EndGameReport, controlProps: {}, hideDashboard: true, }; @@ -416,12 +359,7 @@ export function newEndGameReportPageControl(): PageControl { export interface EndGameReportPageProps extends ControlCallbacks, GameSettings { - capitalFundingState: CapitalFundingState, - trackedStats: TrackedStats, - defaultTrackedStats: TrackedStats, - completedProjects: CompletedProject[]; - implementedRenewableProjects: RenewableProject[]; - implementedFinancedProjects: ImplementedProject[]; + endGameResults: EndGameResults; yearRangeInitialStats: TrackedStats[]; } diff --git a/src/components/LoseGame.tsx b/src/components/LoseGame.tsx new file mode 100644 index 0000000..169d13e --- /dev/null +++ b/src/components/LoseGame.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { Swiper, SwiperSlide, useSwiper } from 'swiper/react'; +import 'swiper/css'; +import 'swiper/css/pagination'; +import 'swiper/css/navigation'; +import 'swiper/css/effect-fade'; +import './SwiperOverrides.css'; +import 'animate.css'; +import { PageControl } from './controls'; +import { EndGameResults } from '../trackedStats'; + + +export default class LoseGame extends React.Component { + + render() { + return ( +
+ + +
+ {/*
*/} +
+ Your company has reduced CO2e emissions by {`${this.props.endGameResults.carbonSavingsPercent}%`} in 10 years +
+ +
+ You were unable to meet your goal of decarbonizing by 50% +
+ +
+ Try again? +
+
+ +
+
+
+
+ ); + } + +} + +export interface LoseGameProps { + endGameResults: EndGameResults; +} + +export function newLoseGameControl(): PageControl { + return { + componentClass: LoseGame, + controlProps: {}, + hideDashboard: true, + }; +} diff --git a/src/components/SwiperOverrides.css b/src/components/SwiperOverrides.css new file mode 100644 index 0000000..e1614df --- /dev/null +++ b/src/components/SwiperOverrides.css @@ -0,0 +1,28 @@ + +:root { + --swiper-theme-color: #fff; + --swiper-pagination-bullet-inactive-color: #e6e6e6; + --swiper-pagination-bullet-inactive-opacity: .4; + --swiper-navigation-size: 84px; + --swiper-navigation-top-offset: 50%; + --swiper-navigation-sides-offset: 10px; + --swiper-navigation-color: var(--swiper-theme-color); +} + +.swiper-pagination-bullet { + width: 24px; + height: 24px; +} + +.swiper-pagination-bullet-active { + color: #fff !important; +} + + +/* .swiper-button-prev, +.swiper-button-next { + color: #fff ; + width: calc(var(--swiper-navigation-size) / 44*75) ; + height: calc(var(--swiper-navigation-size) * 3); + font-weight: 1000; +} */ diff --git a/src/components/WinGame.tsx b/src/components/WinGame.tsx new file mode 100644 index 0000000..0c2f299 --- /dev/null +++ b/src/components/WinGame.tsx @@ -0,0 +1,251 @@ +// Import Swiper React components +import { Swiper, SwiperSlide, useSwiper } from 'swiper/react'; +import { Navigation, Pagination, Scrollbar, A11y, Autoplay, EffectFade } from 'swiper/modules'; +// Import Swiper styles +import 'swiper/css'; +import 'swiper/css/pagination'; +import 'swiper/css/navigation'; +import 'swiper/css/effect-fade'; +import './SwiperOverrides.css'; +import 'animate.css'; + +import { PageControl } from './controls'; +import React, { Fragment, ReactNode } from 'react'; +import { EndGameResults } from '../trackedStats'; + + +export default class WinGame extends React.Component { + progressCircle; + progressContent; + + constructor(props) { + super(props); + this.progressCircle = React.createRef(); + this.progressContent = React.createRef(); + } + + + onAutoplayTimeLeft = (s, time, progress) => { + this.progressCircle.current.style.setProperty('--progress', 1 - progress); + this.progressContent.current.textContent = `${Math.ceil(time / 1000)}s`; + }; + + + render() { + return ( +
+ {/* // todo deal with prefers reduced motion */} + } + centeredSlides={true} + autoplay={{ + delay: 9000, + disableOnInteraction: true, + }} + pagination={{ + clickable: true + }} + // onAutoplay={(swiper) => { + // }} + onAutoplayTimeLeft={this.onAutoplayTimeLeft} + navigation={true} + className='mySwiper' + > + + + Your company has met the decarbonization goal, reducing CO2e emissions by + + +
+ ] + } + /> + + + + While spending + + on GHG reduction measures, your cost per kg reduced was + + + + + ] + } + /> + + + + {this.props.endGameResults.projectedFinancedSpending? + You are projected to spend + + on financed and renewed projects. + + : + <> + } + , +
+ Your total spend including projections is + + + +
+ ] + } + /> +
+ + + You reduced CO2e emissions by + , +
+ Cost per kg was reduced to +
, +
+ Thank you! +
, + ] + } + /> +
+ +
+ + + + +
+ + + ); + } + +} + +export interface WinGameProps { + endGameResults: EndGameResults; +} + +export function newWinGameControl(): PageControl { + return { + componentClass: WinGame, + controlProps: {}, + hideDashboard: true, + }; +} + +export interface UnderlineProps {text: string, animationClass: string} + +function UnderlineSpan(props: UnderlineProps) { + // todo animationClass was ID animate-underline-emphasis + return ( + +    + console.log('onAnimationStart', e)} + onAnimationIteration={e => console.log('onAnimationIteration', e)} + onAnimationEnd={e => console.log('onAnimationEnd', e)} + className={`${props.animationClass} animate-color`}>{props.text} +    + + ) +} + + +export interface SlideProps {statDivs: ReactNode[]} + +function SlideContent(props: SlideProps) { + return ( +
+
+
+ {props.statDivs.map(div => { + return (div) + })} +
+ + +
+
+ ) +} diff --git a/src/images/electricity.jpg b/src/images/electricity.jpg new file mode 100644 index 0000000..4d8e02e Binary files /dev/null and b/src/images/electricity.jpg differ diff --git a/src/images/field-opt.jpg b/src/images/field-opt.jpg new file mode 100644 index 0000000..627ee0b Binary files /dev/null and b/src/images/field-opt.jpg differ diff --git a/src/images/field.jpeg b/src/images/field.jpeg new file mode 100644 index 0000000..11aef91 Binary files /dev/null and b/src/images/field.jpeg differ diff --git a/src/images/lose-image-bg-2.jpg b/src/images/lose-image-bg-2.jpg new file mode 100644 index 0000000..3386bf4 Binary files /dev/null and b/src/images/lose-image-bg-2.jpg differ diff --git a/src/images/lose-image-opt.jpg b/src/images/lose-image-opt.jpg new file mode 100644 index 0000000..77f756e Binary files /dev/null and b/src/images/lose-image-opt.jpg differ diff --git a/src/images/solar-array.jpg b/src/images/solar-array.jpg new file mode 100644 index 0000000..bb4fc79 Binary files /dev/null and b/src/images/solar-array.jpg differ diff --git a/src/images/solar-array2-opt.jpg b/src/images/solar-array2-opt.jpg new file mode 100644 index 0000000..6143c09 Binary files /dev/null and b/src/images/solar-array2-opt.jpg differ diff --git a/src/images/solar-array2.jpg b/src/images/solar-array2.jpg new file mode 100644 index 0000000..7af67a2 Binary files /dev/null and b/src/images/solar-array2.jpg differ diff --git a/src/images/solar3.jpg b/src/images/solar3.jpg new file mode 100644 index 0000000..f8c321f Binary files /dev/null and b/src/images/solar3.jpg differ diff --git a/src/images/turbine2-opt.jpg b/src/images/turbine2-opt.jpg new file mode 100644 index 0000000..ee72d46 Binary files /dev/null and b/src/images/turbine2-opt.jpg differ diff --git a/src/images/turbine2.jpg b/src/images/turbine2.jpg new file mode 100644 index 0000000..f5c637d Binary files /dev/null and b/src/images/turbine2.jpg differ diff --git a/src/trackedStats.tsx b/src/trackedStats.tsx index 44b01da..2b4e679 100644 --- a/src/trackedStats.tsx +++ b/src/trackedStats.tsx @@ -1,3 +1,4 @@ +import { CompletedProject } from './ProjectControl'; import { theme } from './components/theme'; /** @@ -108,6 +109,17 @@ export interface TrackedStats { renewedProjectCostSavingsMultiplier: number; } +export interface EndGameResults { + carbonSavingsPercent: string, + gameTotalSpending: string, + projectedFinancedSpending: string, + gameCurrentAndProjectedSpending: string, + costPerCarbonSavings: string, + completedProjects: CompletedProject[]; + endYearStats: TrackedStats; + isWinningGame: boolean; +} + export interface YearCostSavings { naturalGas: number, electricity: number,