Skip to content

Commit

Permalink
Demo using Next.js to hide the API key on the server side.
Browse files Browse the repository at this point in the history
  • Loading branch information
vengroff committed Dec 22, 2024
1 parent 5856a42 commit 814129c
Show file tree
Hide file tree
Showing 20 changed files with 6,401 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ docs/_autosummary

# Notebook checkpoints.
.ipynb_checkpoints

# Node modules.
node_modules

# Builds
build/
41 changes: 41 additions & 0 deletions rem-with-nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
30 changes: 30 additions & 0 deletions rem-with-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# REM React Demo

This demonstration illustrates how to use [react](https://react.dev/) to call
Rewiring America's Residential Electrification(REM) API. It constructs a web page
that allows users to type in their address and current heating fuel and get an
estimate of how much they could save by switching to a heat pump.

To the user, the final result looks a lot like our [HTML/JavaScript demo](../www)
and our [React Demo](../react).

The difference is that this version is written in [Next.js](https://nextjs.org)
and takes advantage of it's unique `'use client'` and `'use server'` annotations
to make sure that part of the code runs on the server and part runs on the client.
The advantage of this is that the API key we use to call the REM API is never
visible on the client side. So nobody can see it simply by loading our page the
way they could with the [React Demo](../react).

To run this project in a development server on port 3000 of `localhost`, use
T
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Binary file added rem-with-nextjs/app/favicon.ico
Binary file not shown.
101 changes: 101 additions & 0 deletions rem-with-nextjs/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--background: #ffffff;
--foreground: #171717;
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

html, body {
height: 100%;
margin: 16pt;
color: #333;
font-family: 'Balsamiq Sans';
}

#titlebar {
margin-left: auto;
margin-right: auto;
width: 90%;
position: relative;
}

#titlebar h1 {
text-align: center;
font-size: 36px;
padding: 0px;
margin: 10px;
}

#titlebar h2 {
text-align: center;
font-size: 24px;
padding: 0px;
margin: 10px;
}

#content {
margin-left: auto;
margin-right: auto;
width: 50%;
min-width: 540px;
max-width: 640px;
position: relative;
}

#mainform {
margin-left: auto;
margin-right: auto;
width: 80%;
font-size: 14pt;
}

#address {
margin-left: auto;
margin-right: auto;
width: 460px;
height: 2em;
border: 2px solid #333;
padding: 2px;
margin-bottom: 1em;
}

#ok {
width: 80px;
background-color: #eee;
border: 2px solid #333;
}

.fuel {
margin: 0.25em;
}

#results {
margin-top: 40px;
}

#savings {
font-size: 18pt;
}

#addapi {
margin-top: 200px;
float: right;
}

p.label {
margin-block-start: 1em;
margin-block-end: 0.25em;
}

.wait {
cursor: wait;
}
30 changes: 30 additions & 0 deletions rem-with-nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Metadata } from "next";
import { Balsamiq_Sans } from "next/font/google";
import "./globals.css";

const balsmiqSans = Balsamiq_Sans({
variable: "--font-balsamiq-sans",
subsets: ["latin"],
weight: [ "400", "700" ]
})

export const metadata: Metadata = {
title: "REM API Demo",
description: "Demonstrate using a next.js server-side function to avoid exposing API keys.",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${balsmiqSans.variable} antialiased`}
>
{children}
</body>
</html>
);
}
130 changes: 130 additions & 0 deletions rem-with-nextjs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client';
// import Image from "next/image";

import Head from 'next/head'

import { useState } from 'react';
import './globals.css';

import serverSavings from './serverSavings';

async function clientSavings(address : string, currentFuel : string) {
console.log("On the client side.");

const expectedSavings = await serverSavings(address, currentFuel);

console.log(`Client savings: ${expectedSavings}`);

return expectedSavings
}

function AddressForm() {
// The three values we need to call the REM API are
//
// - the address of the home;
// - the current heating fuel;
// - the upgrade to be performed.
//
// We will make states for the first two, whose values
// will be managed by a form. For the third one, we will
// just use a constant value.
const [address, setAddress] = useState("");
const [currentFuel, setCurrentFuel] = useState("natural_gas");
const [savings, setSavings] = useState("")
const [hidden, setHidden] = useState(true)

const onFuelChange = (event) => {
setCurrentFuel(event.target.value)
setHidden(true)
}

/**
* Callback when the form is submitted.
*
* This is where we make the API call, and then,
* when it returns, update the results in the DOM
* based on the result.
*
* @param {Event} event - the change event.
*/
const handleSubmit = (event) => {
event.preventDefault();

const expectedSavings = clientSavings(address, currentFuel);

console.log(`Handler Savings: ${expectedSavings}`)

setSavings(expectedSavings)
setHidden(false)
};

return (
<>
<div id="mainform">
<form onSubmit={handleSubmit}>
<label>
<p className="label">
Address:
</p>
<input
type="text"
id="address"
value={address}
onChange={
e => {
setAddress(e.target.value)
setHidden(true)
}
}
/>
</label>
<label>
<p className="label">
Current Heating Fuel:
</p>
<input type="radio" className="fuel" name="fuel" value="fuel_oil" onChange={onFuelChange} />
Fuel Oil &nbsp;&nbsp;
<input type="radio" className="fuel" name="fuel" value="natural_gas" onChange={onFuelChange} defaultChecked={true} />
Natural Gas &nbsp;&nbsp;
<input type="radio" className="fuel" name="fuel" value="propane" onChange={onFuelChange} />
Propane &nbsp;&nbsp;
<input type="radio" className="fuel" name="fuel" value="electricity" onChange={onFuelChange} />
Electricity
</label>
<br/><br/>
<label>
<input type="submit" value="OK" id="ok"/>
</label>
</form>
</div>
<div id="results" hidden={hidden}>
<p>Expected annual savings: <span id="savings">{savings}</span></p>
<p>Call us at 1-800-LEC-TRIC to get your job started today!</p>
</div>
</>
);
}

export default function Home() {
return (
<div>
<Head>
<title>REM Demo with Secrets</title>
</Head>
<div id="titlebar">
<h1 id="title">
Electrification Nation
</h1>
<h2 id="subtitle">Your Heat Pump People</h2>
</div>
<div id="content">
<div>
We're here to help you save money by electrifying your home. Enter your address and select the fuel you
currently use to heat your home and we'll tell you how much you can save by having us install a heat pump!
</div>
<AddressForm>
</AddressForm>
</div>
</div>
);
}
37 changes: 37 additions & 0 deletions rem-with-nextjs/app/serverSavings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use server';

import axios from "axios";

export default async function serverSavings(address : string, currentFuel: string) {

console.log("On the server side.")

const upgrade = 'high_eff_hp_elec_backup';

// This is the URL for the REM API.
const remApiURL = "https://api.rewiringamerica.org/api/v1/rem/address";

// If you don't have an API key, please register at
// https://rewiring.link/api-signup to get one.
const apiKey = "INSERT_YOUR_API_KEY_HERE";

let expectedSavings = "123";

await axios
.get(
remApiURL,
{
params: {address: address, heating_fuel: currentFuel, upgrade: upgrade},
headers: {Authorization: "Bearer " + apiKey}
}
)
.then(
(response) => {
const rawSavings = -Number(response.data.fuel_results.total.delta.cost.mean.value)
const roundedSavings = (Math.round(rawSavings * 100) / 100).toFixed(2);
expectedSavings = "$" + roundedSavings
}
)

return expectedSavings;
}
Loading

0 comments on commit 814129c

Please sign in to comment.