Millions of Americans plan camping vacations across the United States each year. Campsnatch is a website that helps campers get notifications when campgrounds become available in their desired camping destinations. Campsnatch leverages the recreation.gov apis to scan for campsite availabilities.
- Search for thousands of campgrounds across the America
- Get detailed information about campgrounds including amentities, alerts, activities, and directions
- Set up a tracker to enable notifications for a certain campground
- View and edit list of enabled trackers for campgrounds.
- The html table element can produce a table grid structure to format rows and columns of data
- thead stand for table heading
- tr stands for table row
- td stands for table data
- tbody stands for table body
Player | Gloobles | Za'taak |
---|---|---|
TR-7 | 7 | 4,569 |
Khiresh Odo | 7 | 7,223 |
Mia Oolong | 9 | 6,219 |
Use frameworks like tailwinds and bootstrap to leverage their design components. This allows you to not have to start from scratch. Style sheets are connected up in the head of the html document by using "link rel="stylesheet" href="styles.css""
localstorage allows you to set persistent properties in the browser window.location.href = "url" lets you jump pages in the javascript file document.querySelectorAll allows you to grab all items with a class name document.querySelectore('.classname') allow you to grab a class name audio files can be played with the Audio constructor and the play property
Nodemon Once you start writing complex web applications you will find yourself making changes in the middle of debugging sessions and you would like have node restart automatically and update the browser as the changes are saved. This seems like a simple thing, but over the course of hundreds of changes, every second you can save really starts to add up.
The Nodemon package is basically a wrapper around node that watches for files in the project directory to change. When it detects that you saved something it will automatically restart node.
If you would like to experiment with this then take the following steps. First install Nodemon globally so that you can use it to debug all of your projects.
npm install -g nodemon Then, because VS Code does not know how to launch nodemon automatically, you need create a VS Code launch configuration. In VS Code press CTRL-SHIFT-P (on Windows) or β-β§-P (on Mac) and type the command Debug: Add configuration. This will then ask you what type of configuration you would like to create. Type Node.js and select the Node.js: Nodemon setup option. in the launch configuration file at it creates, change the program from app.js to main.js (or whatever the main JavaScript file is for your application) and save the configuration file.
Now when you press F5 to start debugging it will run Nodemon instead of Node.js and your changes will automatically update your application when you save.
Converting Simon to a service involved the following steps.
-
Move all the previous deliverable code files (_.html, _.js, *.css, favicon.ico, and asserts) into a sub-directory named
public
. We will use the HTTP Node.js based service to host the front-end application files. This is done with the static file middleware that we will add our serviceindex.js
.app.use(express.static('public'));
When running our service the static file middleware takes care of reading the front-end code from the
public
directory and returning it to the browser. The service only directly handles the endpoint requests. -
Within the project directory run
npm init -y
. This configures the directory to work with node.js. -
Modify or create
.gitignore
to ignorenode_modules
. -
Install the Express package by running
npm install express
. This will write the Express package dependency in thepackage.json
file and install all the Express code to thenode_modules
directory. -
Create a file named
index.js
in the root of the project. This is the entry point that node.js will call when you run your web service. -
Add the basic Express JavaScript code needed to host the application static content and the desired endpoints.
const express = require('express'); const app = express(); // The service port. In production the front-end code is statically hosted by the service on the same port. const port = process.argv.length > 2 ? process.argv[2] : 3000; // JSON body parsing using built-in middleware app.use(express.json()); // Serve up the front-end static content hosting app.use(express.static('public')); // Router for service endpoints const apiRouter = express.Router(); app.use(`/api`, apiRouter); // GetScores apiRouter.get('/scores', (_req, res) => { res.send(scores); }); // SubmitScore apiRouter.post('/score', (req, res) => { scores = updateScores(req.body, scores); res.send(scores); }); // Return the application's default page if the path is unknown app.use((_req, res) => { res.sendFile('index.html', { root: 'public' }); }); app.listen(port, () => { console.log(`Listening on port ${port}`); });
-
Modify the Simon application code to make service endpoint requests to our newly created HTTP service code.
async function loadScores() { const response = await fetch("/api/scores") const scores = await response.json() // Modify the DOM to display the scores
A collection is basically an array that stores documents
[
{
_id: '62300f5316f7f58839c811de',
name: 'Lovely Loft',
summary: 'A charming loft in Paris',
beds: 1,
last_review: {
$date: '2022-03-15T04:06:17.766Z',
},
price: 3000,
},
{
_id: '623010b97f1fed0a2df311f8',
name: 'Infinite Views',
summary: 'Modern home with infinite views from the infinity pool',
property_type: 'House',
beds: 5,
price: 250,
},
];
// find all houses
db.house.find();
// find houses with two or more bedrooms
db.house.find({ beds: { $gte: 2 } });
// find houses that are available with less than three beds
db.house.find({ status: 'available', beds: { $lt: 3 } });
// find houses with either less than three beds or less than $1000 a night
db.house.find({ $or: [(beds: { $lt: 3 }), (price: { $lt: 1000 })] });
// find houses with the text 'modern' or 'beach' in the summary
db.house.find({ summary: /(modern|beach)/i });
π Deeper dive reading: MongoDB tutorial
The first step to using Mongo in your application is to install the mongodb
package using NPM.
β npm install mongodb
With that done you then use the MongoClient
object to make a client connection to the database server. This requires a username, password, and the hostname of the database server.
const { MongoClient } = require('mongodb');
const userName = 'holowaychuk';
const password = 'express';
const hostname = 'mongodb.com';
const uri = `mongodb+srv://${userName}:${password}@${hostname}`;
const client = new MongoClient(uri);
With the client connection you can then get a database object and from that a collection object. The collection object allows you to insert, and query for, documents. You do not have to do anything special to insert a JavaScript object as a Mongo document. You just call the insertOne
function on the collection object and pass it the JavaScript object. When you insert a document, if the database or collection does not exists, Mongo will automatically create them for you. When the document is inserted into the collection it will automatically be assigned a unique ID.
const collection = client.db('rental').collection('house');
const house = {
name: 'Beachfront views',
summary: 'From your bedroom to the beach, no shoes required',
property_type: 'Condo',
beds: 1,
};
await collection.insertOne(house);
To query for documents you use the find
function on the collection object. Note that the find function is asynchronous and so we use the await
keyword to wait for the promise to resolve before we write them out to the console.
const cursor = collection.find();
const rentals = await cursor.toArray();
rentals.forEach((i) => console.log(i));
If you do not supply any parameters to the find
function then it will return all documents in the collection. In this case we only get back the single document that we previously inserted. Notice that the automatically generated ID is returned with the document.
Output
[
{
_id: new ObjectId('639a96398f8de594e198fc13'),
name: 'Beachfront views',
summary: 'From your bedroom to the beach, no shoes required',
property_type: 'Condo',
beds: 1,
},
];
You can provide a query and options to the find
function. In the example below we query for a property_type
of Condo that has less than two bedrooms. We also specify the options to sort by descending price, and limit our results to the first 10 documents.
const query = { property_type: 'Condo', beds: { $lt: 2 } };
const options = {
sort: { price: -1 },
limit: 10,
};
const cursor = collection.find(query, options);
const rentals = await cursor.toArray();
rentals.forEach((i) => console.log(i));
The query matches the document that we previously inserted and so we get the same result as before.
There is a lot more functionality that MongoDB provides, but this is enough to get you started. If you are interested you can explore the tutorials on their website.
You need to protect your credentials for connecting to your Mongo database. One common mistake is to check them into your code and then post it to a public GitHub repository. Instead you can load your credentials when the application executes. One common way to do that, is to read them from environment variables. The JavaScript process.env
object provides access to the environment.
const userName = process.env.MONGOUSER;
const password = process.env.MONGOPASSWORD;
const hostname = process.env.MONGOHOSTNAME;
if (!userName) {
throw Error("Database not configured. Set environment variables");
}
Following this pattern requires you to set these variables in your development and production environments before you can successfully execute.
const { MongoClient } = require('mongodb');
const uuid = require('uuid');
const bcrypt = require('bcrypt');
const cookieParser = require('cookie-parser');
const express = require('express');
const app = express();
const userName = process.env.MONGOUSER;
const password = process.env.MONGOPASSWORD;
const hostname = process.env.MONGOHOSTNAME;
const url = `mongodb+srv://${userName}:${password}@${hostname}`;
const client = new MongoClient(url);
const collection = client.db('authTest').collection('user');
app.use(cookieParser());
app.use(express.json());
// createAuthorization from the given credentials
app.post('/auth/create', async (req, res) => {
if (await getUser(req.body.email)) {
res.status(409).send({ msg: 'Existing user' });
} else {
const user = await createUser(req.body.email, req.body.password);
setAuthCookie(res, user.token);
res.send({
id: user._id,
});
}
});
// loginAuthorization from the given credentials
app.post('/auth/login', async (req, res) => {
const user = await getUser(req.body.email);
if (user) {
if (await bcrypt.compare(req.body.password, user.password)) {
setAuthCookie(res, user.token);
res.send({ id: user._id });
return;
}
}
res.status(401).send({ msg: 'Unauthorized' });
});
// getMe for the currently authenticated user
app.get('/user/me', async (req, res) => {
authToken = req.cookies['token'];
const user = await collection.findOne({ token: authToken });
if (user) {
res.send({ email: user.email });
return;
}
res.status(401).send({ msg: 'Unauthorized' });
});
function getUser(email) {
return collection.findOne({ email: email });
}
async function createUser(email, password) {
const passwordHash = await bcrypt.hash(password, 10);
const user = {
email: email,
password: passwordHash,
token: uuid.v4(),
};
await collection.insertOne(user);
return user;
}
function setAuthCookie(res, authToken) {
res.cookie('token', authToken, {
secure: true,
httpOnly: true,
sameSite: 'strict',
});
}
const port = 8080;
app.listen(port, function () {
console.log(`Listening on port ${port}`);
});
The cookie-parser package provides middleware for cookies and so we will leverage that. We import the cookieParser object and then tell our app to use it. When a user is successfully created, or logs in, we set the cookie header. Since we are storing an authentication token in the cookie we want to make it as secure as possible, and so we use the httpOnly, secure, and sameSite options. httpOnly tells the browser to not allow JavaScript running on the browser to read the cookie. secure requires HTTPS to be used when sending the cookie back to the server. sameSite will only return the cookie to the domain that generated it.
To hash our passwords we will use the bcrypt package. This creates a very secure one way hash of the password. If you are interested in understanding how bcrypt works, it is definitely worth the time.
To generate a reasonable authentication token we use the uuid package. UUID stands for Universally Unique Identifier, and it does a really good job creating a hard to guess, random, unique ID.
JavaScript running on a browser can initiate a websocket connection with the browser's WebSocket API. First you create a WebSocket object by specifying the port you want to communicate on.
You can then send messages with the send
function, and register a callback using the onmessage
function to receive messages.
const socket = new WebSocket('ws://localhost:9900');
socket.onmessage = (event) => {
console.log('received: ', event.data);
};
socket.send('I am listening');
The server uses the ws
package to create a WebSocketServer that is listening on the same port the browser is using. By specifying a port when you create the WebSocketServer you are telling the server to listen for HTTP connections on that port and to automatically upgrade them to a WebSocket connection if the request has a connection: Upgrade
header.
When a connection is detected it calls the server's on connection
callback. The server can then send messages with the send
function, and register a callback using the on message
function to receive messages.
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 9900 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const msg = String.fromCharCode(...data);
console.log('received: %s', msg);
ws.send(`I heard you say "${msg}"`);
});
ws.send('Hello webSocket');
});
π Recommended reading: React.dev - Your First Component
React components allow you to modularize the functionality of your application. This allows the underlying code to directly represent the components that a user interacts with. It also enables code reuse as common application component often show up repeatedly.
One of the primary purposes of a component is to generate user interface. This is done with the components render
function. Whatever is returned from the render function is inserted into the component HTML element.
As a simple example, a JSX file containing a React component element named Demo
would cause React to load the Demo
component, call the render function, and insert the result into the place of the Demo
element.
JSX
<div>
Component: <Demo />
</div>
Notice that Demo
is not a valid HTML element. The transpiler will replace this tag with the resulting rendered HTML.
React component
function Demo() {
const who = 'world';
return <b>Hello {who}</b>;
}
Resulting HTML
<div>Component: <b>Hello world</b></p>
React components also allow you to pass information to them in the form of element properties. The component receives the properties in its constructor and then can display them when it renders.
JSX
<div>Component: <Demo who="Walke" /><div>
React component
function Demo(props) {
return <b>Hello {props.who}</b>;
}
Resulting HTML
<div>Component: <b>Hello Walke</b></div>
In addition to properties, a component can have internal state. Component state is created by calling the React.useState
hook function. The useState function returns a variable that contains the current state and a function to update the state. The following example creates a state variable called clicked
toggles the click state in the updateClicked
function that gets called when the paragraph text is clicked.
const Clicker = () => {
const [clicked, updateClicked] = React.useState(false);
const onClicked = (e) => {
updateClicked(!clicked);
};
return <p onClick={(e) => onClicked(e)}>clicked: {`${clicked}`}</p>;
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clicker />);
You should note that you can use JSX even without a function. A simple variable representing JSX will work anyplace you would otherwise provide a component.
const hello = <div>Hello</div>;
ReactDOM.render(hello, document.getElementById('root'));
π Required reading: React Router DOM Tutorial
A web framework router provides essential functionality for single page applications. With a multiple web page application the headers, footers, navigation, and common components must be either duplicated in each HTML page, or injected before the server sends the page to the browser. With a single page application the browser only loads one HTML page and then JavaScript is used to manipulate the DOM and give it the appearance of multiple pages. The router defines the routes a user can take through the application, and automatically manipulates the DOM to display the appropriate framework components.
React does not have a standard router package, and there are many that you can choose from. We will use react-router-dom Version 6. The simplified routing functionality of React-router-dom derives from the project react-router for its core functionality. Do not confuse the two, or versions of react-router-dom before version 6, when reading tutorials and documentation.
A basic implementation of the router consists of a BrowserRouter
component that encapsulates the entire application and controls the routing action. The Link
component captures user navigation events and modifies what is rendered by the Routes
component by matching up the to
and path
attributes.
// Inject the router into the application root DOM element
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
// BrowserRouter component that controls what is rendered
// NavLink component captures user navigation requests
// Routes component defines what component is routed to
<BrowserRouter>
<div className='app'>
<nav>
<NavLink to='/'>Home</Link>
<NavLink to='/about'>About</Link>
<NavLink to='/users'>Users</Link>
</nav>
<main>
<Routes>
<Route path='/' element={<Home />} exact />
<Route path='/about' element={<About />} />
<Route path='/users' element={<Users />} />
<Route path='*' element={<Navigate to='/' replace />} />
</Routes>
</main>
</div>
</BrowserRouter>
);