Skip to content

Commit ce3712e

Browse files
committed
fix merge conflict
2 parents 0115297 + 4c817ce commit ce3712e

32 files changed

+845
-428
lines changed

.DS_Store

2 KB
Binary file not shown.

README.md

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Tappdin: A Beer Tracking App
2+
*By [Johnny Bui](https://github.com/JBui923) (front-end lead) [Giancarlo Sanchez](https://github.com/giancarlo-sanchez) (backend lead), and [Ben Perlmutter](https://github.com/bpmutter) (project manager)*
3+
* [Live version](http://tappdin.herokuapp.com/)
4+
* [Backend Github repo](https://github.com/bpmutter/tappdin-backend)
5+
* [Project Overview Presentation](https://docs.google.com/presentation/d/10oj08Ui1VeKpdLh826dOvSCKTy7ddCq2_fsc34zgWfU/edit?usp=sharing)
6+
7+
**Table of Contents**:
8+
* [Tappdin at a Glance](https://github.com/bpmutter/tappdin/blob/master/README.md#tappdin-at-a-glance)
9+
* [Application Architecture and Technologies Used](https://github.com/bpmutter/tappdin/blob/master/README.md#application-architecture-and-technologies-used)
10+
* [Frontend Overview](https://github.com/bpmutter/tappdin/blob/master/README.md#frontend-overview)
11+
* [Backend Overview](https://github.com/bpmutter/tappdin/blob/master/README.md#backend-overview)
12+
* [Conclusion & Next Steps](https://github.com/bpmutter/tappdin/blob/master/README.md#conclusion)
13+
## Tappdin At a Glance
14+
Tappdin is a beer tracking app modeled on [Untappd](https://untappd.com/). It allows users to create accounts, post and delete checkins of beers, view the checkins of other users, and discover new beers.
15+
16+
Tappdin currently possesses a database of 500 beers from almost 20 breweries that users can explore and review, which we call **checkins** to maintain consistency with the original Untappd app.
17+
18+
## Application Architecture and Technologies Used
19+
Tappdin was built using separate front and back end servers that communicate via RESTful APIs.
20+
21+
Both front and backend servers are built using the Express NodeJS framework. We used a PostgreSQL (postgres) database to store all application data.
22+
23+
The front end uses the [Pug](https://pugjs.org/api/getting-started.html) templating engine to render views from the frontend server. We used vanilla Javascript for interactivity and standard CSS for styling.
24+
25+
The backend uses a suite of libraries for application security and building its API routes (discussed further in the backend section below). To connect our backend to the postgres database we implemented the [Sequelize ORM](https://sequelize.org/). We also seeded the database using beer and brewery information from [BreweryDB API](https://brewerydb.com/developers/).
26+
27+
![Application architecture](/readme-assets/application-architecture.png)
28+
29+
## Frontend Overview
30+
As Tappdin, is a quite straightforward CRUD app with simple interactivity, we were able to build out the front end without any AJAX and minimal client-side Javascript. There are actually only 6 lines of frontend Javascript, which were used for the 'Demo User' login button!
31+
32+
We made extensive use of the **Pug templating engine** to render dynamic content and create reusable HTML components that we were able to deploy across multiple views on the site.
33+
34+
### Dynamic Templating with Pug
35+
For instance, we created a Pug mixin (the Pug equivalent of a JS function) to create relevant checkins across different views. We paired this with a component that took an array of checkin objects to dynamically render the associated checkins in its context (checkins by user on homepage, checkins about brewery on brewery page, etc.)
36+
37+
Pug code snippet of checkins mixin:
38+
```pug
39+
mixin checkin(checkin)
40+
.checkin
41+
head
42+
link(rel="stylesheet" type="text/css" href="/styles/checkin.css")
43+
img.checkin__profile-picture(src=checkin.User.photo)
44+
.checkin__main
45+
p #[a(href=`/users/${checkin.User.id}`) #{checkin.User.firstName} #{checkin.User.lastName}] drank #[a(href=`/beers/${checkin.Beer.id}`) #{checkin.Beer.name}] from #[a(href=`/breweries/${checkin.Beer.Brewery.id}`) #{checkin.Beer.Brewery.name}]
46+
.checkin__rating
47+
p Rating:
48+
span.checkin__rating-val=checkin.displayRating
49+
//-created from the script
50+
if checkin.comment
51+
p=checkin.comment
52+
else
53+
p No comment
54+
div.checkin__other-info
55+
span.checkin__date=checkin.createdAt
56+
if checkin.isSessionUser
57+
span.checkin__delete #[a(href=`/checkins/${checkin.id}/delete`) Delete checkin]
58+
img.checkin__profile-picture(src=checkin.Beer.Brewery.image)
59+
```
60+
Pug code snippet of dynamically rendering all the checkins for a particular view:
61+
```pug
62+
section.recent-activity
63+
include checkin
64+
head
65+
link(rel="stylesheet" type="text/css" href="/styles/recent-activity.css")
66+
h2 Recent Activity
67+
div#checkin__container
68+
if checkins.length
69+
each checkin in checkins
70+
+checkin(checkin)
71+
else
72+
p It looks like there aren't any reviews yet
73+
```
74+
75+
![Example beer checkin](/readme-assets/checkin-example.png)
76+
77+
### Refactoring the Way to Reusable Code
78+
The most challenging aspect of the front end of Tappdin was probably the sheer number of different views that we had to create, and how to serve content dynamically into them.
79+
80+
This required us to get creative in how we created that content, using Pug mixins, various layouts, and a whole lot of CSS code. We also had to figure out how to send the relevant data to the views form the server to make code reusable across different views.
81+
82+
There was no secret sauce that we used to make the code module and reusable. Our general process was:
83+
1. hard coding how we wanted something to look with Pug, CSS, and dummy data provided inline.
84+
2. Then we would refactor to add dynamic data from the server
85+
3. Finally, we’d modularize the component to reuse accross other parts of the site.
86+
87+
88+
## Backend Overview
89+
Our backend was primarily a collection of RESTful APIs that we used to query our database for relevant data on beer, breweries, and app users.
90+
91+
We made extensive use of the Sequelize ORM to make fairly complex queries of data associated across multiple tables. Once we had the database built and setup in the ORM, these queries were fairly straightforward, if somewhat challenging to execute due to the sometimes obtuse Sequelize syntax.
92+
93+
### Authentication and Application Security
94+
We used a JSON Web Token (JWT) to authorize our users across sessions. We stored the JWT in a cookie in the browser, which we would send along for verification with backend server requests. We used the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) node library for this.
95+
96+
We also used the [csurf](https://www.npmjs.com/package/csurf) package on our frontend server to protect against CSURF attacks and the [bcryot](https://www.npmjs.com/package/bcrypt) hashing library to protect user passwords.
97+
### Relational Database Model
98+
One of the larger challenges of the project was to design a relational database schema to associate our data. Before we wrote a single line of code, we designed the database with all the tables we’d need and their relationships to each other.
99+
100+
We then had to translate that to 0ur Sequelize models where we we created associations between the tables so that we easily query across them (basically the Sequelize version of standard postgres INNER JOIN).
101+
102+
This was the final database schema:
103+
![Tappdin database schema](readme-assets/database-schema.png)
104+
105+
**Notes on the database schema**:
106+
* Foreign keys are denoted by FK
107+
* Yellow boxes represent many-to-many join tables
108+
* Blue boxes have one-to-many relationships with associated foreign keys
109+
* As of writing (5/24/20), Tappdin doesn’t yet have implemented the Liked Brewery and beer List functionality. However, these tables are in the database, and the relations are set up in the Sequelize models. We would like to implement this functionality at a later point.
110+
111+
### Seeding the Database
112+
Seeding the database was probably the most technically intensive part of the entire project. As noted above, we used the BreweryDB API to seed our database with information about beers and breweries.
113+
114+
While BreweryDB was a great (and free!) resource for generating seed data, the way that the data was structured in BreweryDB was not compatible with our database design.
115+
116+
BreweryDB used 6-character strings as the primary keys for their breweries, whereas we had to use integers at primary keys (PKs) due to restrictions in the Sequelize ORM only permitting integer PKs.
117+
118+
Matters were further complicated by the fact that we needed to seed the breweries for the database before we seeded the beer to allow for the foreign keys (FKs) in the Beers table to be dependent on the Breweries table. However, the relationships within BreweryDB API structure required that we query beers before we could access the dependent breweries.
119+
120+
We therefore had to:
121+
1. Create a list of all breweries we were to use in our seed data.
122+
2. Add integer primary keys to each brewery instance
123+
3. Reassociate these brewery primary keys with their associated beers
124+
125+
To solve this problem, we first did a pass through all 500 beers we were going to use where we created a [Javascript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) to capture only the unique breweries.
126+
127+
**Seed Set Code:**
128+
```js
129+
const seed = require('./raw-data')
130+
131+
const breweriesSet = new Set();
132+
seed.forEach(beer => {
133+
if (beer.breweries && beer.breweries[0].id) {
134+
breweriesSet.add(
135+
beer.breweries[0].id
136+
)
137+
}
138+
});
139+
140+
```
141+
142+
We then converted that set into an array, so we could easily create PKs from the position in the array. Our PKs, were just the position+1.
143+
144+
**Converting the set into useable array:**
145+
```js
146+
const brewerySeed = [];
147+
seed.forEach(beer => {
148+
if (beer.breweries && beer.breweries[0].id) {
149+
if (breweriesSet.has(beer.breweries[0].id)) {
150+
brewerySeed.push({
151+
name: beer.breweries[0].name,
152+
key: beer.breweries[0].id,
153+
location: `${beer.breweries[0].locations[0].locality}, ${beer.breweries[0].locations[0].region}`,
154+
description: beer.breweries[0].description,
155+
website: beer.breweries[0].website,
156+
image: beer.breweries[0].images ? beer.breweries[0].images.squareLarge : null,
157+
createdAt: new Date(),
158+
updatedAt: new Date()
159+
});
160+
breweriesSet.delete(beer.breweries[0].id);
161+
}
162+
}
163+
});
164+
```
165+
Once we had the brewery data in a Sequelize-compatible format, we reassociated it with the beer table, adding the relevant beer as a FK referenced by it’s ID.
166+
167+
We then removed and/or renamed keys in the beer and brewery JSON objects to be compatible with the database schema.
168+
169+
Finally, we wrote the data out into JS files, in which we exported the arrays of beer/brewery POJOs to the Sequelize seed file, from where we added it to the database.
170+
## Conclusion
171+
This project represented our first full-stack application. It was, to put it modestly, a challenge. But with that being said, we would also consider it to be a very successful effort—we met our MVP goals and created a full-stack CRUD application that now lives on the internet, and a pretty decent looking one too.
172+
173+
While the project has been deployed that doesn’t mean we are done with it yet. A couple of features that we haven’t been able to add, but would like to are:
174+
* Make the whole project responsive
175+
* Add functionality for Liked Breweries and Lists of beers (already built into database, as noted above, but not yet implemented in app)
176+
* Refine search functionality
177+
* Comprehensively review error handling to make sure that we properly handle all
178+
* Add view and backend API to render all beers associated with a particular brewery
179+
180+
Thanks for reading, cheers! 🍻

index.js

+21-19
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const settingsRouter = require('./routes/settings');
77
const searchRouter = require('./routes/search');
88
const cookieParser = require('cookie-parser')
99
const bodyParser = require('body-parser');
10-
const { asyncHandler } = require("./routes/utils")
10+
const { asyncHandler } = require("./routes/utils");
11+
const csrf = require("csurf");
1112

1213
// Create the Express app.
1314
const app = express();
@@ -18,6 +19,8 @@ app.use(express.static(path.join(__dirname, "public")));
1819
app.use(express.json());
1920
app.use(cookieParser());
2021
app.use(bodyParser.urlencoded({ extended: false }));
22+
app.use(bodyParser.json());
23+
const csrfProtection = csrf({ cookie: true });
2124
app.use("/users", userRouter);
2225
app.use("/checkins", checkinRouter);
2326
app.use("/settings", settingsRouter);
@@ -29,7 +32,6 @@ app.locals.backend = process.env.BACKEND_URL;
2932
app.get("/", asyncHandler(async (req, res) => {
3033
const id = parseInt(req.cookies[`TAPPDIN_CURRENT_USER_ID`], 10);
3134
if (!id) return res.redirect("/log-in");
32-
console.log("requesting", process.env.BACKEND_URL);
3335
const data = await fetch(`${process.env.BACKEND_URL}/users/${id}`, {
3436
headers: {
3537
Authorization: `Bearer ${req.cookies[`TAPPDIN_ACCESS_TOKEN`]}`,
@@ -39,11 +41,7 @@ app.get("/", asyncHandler(async (req, res) => {
3941
res.redirect("/log-in");
4042
return;
4143
}
42-
console.log("the user id:", id);
4344
if (id) {
44-
45-
46-
4745
const { user, checkins } = await data.json();
4846
const sessionUser = req.cookies["TAPPDIN_CURRENT_USER_ID"];
4947
checkins.forEach((checkin) => {
@@ -58,7 +56,6 @@ app.get("/", asyncHandler(async (req, res) => {
5856
date = new Date(checkin.createdAt);
5957
checkin.createdAt = date.toDateString();
6058
});
61-
console.log(checkins)
6259
res.render("index", { user, checkins });
6360
} else {
6461
res.render("log-in");
@@ -117,7 +114,7 @@ app.get("/beers/:id(\\d+)", asyncHandler(async (req, res) => {
117114
const json = await data.json();
118115
const { beer, checkins } = json;
119116
beer.numCheckins = checkins.length;
120-
117+
beer.image = beer.image || "/imgs/beer-default.jpg";
121118
if (checkins.length) {
122119
const checkinsScores = checkins.map((checkin) => checkin.rating);
123120
beer.avgRating =
@@ -137,7 +134,7 @@ app.get("/beers/:id(\\d+)", asyncHandler(async (req, res) => {
137134
date = new Date(checkin.createdAt);
138135
checkin.createdAt = date.toDateString();
139136
});
140-
if (!beer.image) beer.image = "/imgs/beer-default.jpg";
137+
141138
}
142139

143140
res.render("beer", { beer, checkins });
@@ -156,9 +153,17 @@ app.get('/breweries/:id(\\d+)', asyncHandler(async (req, res) => {
156153
return
157154
} else {
158155
const json = await data.json();
159-
const { brewery, checkins } = json;
156+
const { brewery, checkins, beer } = json;
157+
brewery.numCheckins = checkins.length;
158+
brewery.numberOfBeers = beer.length;
160159

161160
if (checkins.length) {
161+
const checkinsScores = checkins.map((checkin) => checkin.rating);
162+
brewery.avgRating =
163+
checkinsScores.reduce((sum, rating) => {
164+
sum += rating;
165+
}) / checkins.length;
166+
162167
const sessionUser = parseInt(req.cookies["TAPPDIN_CURRENT_USER_ID"], 10);
163168
checkins.forEach((checkin) => {
164169
if (sessionUser === checkin.userId) checkin.isSessionUser = true;
@@ -172,7 +177,7 @@ app.get('/breweries/:id(\\d+)', asyncHandler(async (req, res) => {
172177
date = new Date(checkin.createdAt);
173178
checkin.createdAt = date.toDateString();
174179
});
175-
180+
if (!brewery.image) brewery.image = "/imgs/beer-default.jpg";
176181
}
177182
res.render("brewery", { brewery, checkins })
178183
}
@@ -182,11 +187,11 @@ app.get('/breweries/:id(\\d+)', asyncHandler(async (req, res) => {
182187

183188
app.get("/create", (req, res) => { res.render("create") });
184189

185-
app.get("/sign-up", (req, res) => {
186-
res.render("sign-up");
190+
app.get("/sign-up",csrfProtection, (req, res) => {
191+
res.render("sign-up",{csrfToken: req.csrfToken()});
187192
});
188-
app.get("/log-in", (req, res) => {
189-
res.render("log-in")
193+
app.get("/log-in",csrfProtection, (req, res) => {
194+
res.render("log-in",{csrfToken: req.csrfToken()})
190195
})
191196

192197
app.get("/profile", (req, res) => {
@@ -236,12 +241,9 @@ app.use((err, req, res, next) => {
236241
});
237242
});
238243

239-
// Define a port and start listening for connections.
240244

241-
var port = Number.parseInt(process.env.PORT, 10) || 8081;
245+
const port = Number.parseInt(process.env.PORT, 10) || 8081;
242246
app.listen(port, () => {
243247
console.log(`Listening for requests on port ${port}...`);
244248
});
245-
// const port = 4000;
246249

247-
// app.listen(port, () => console.log(`Listening on port ${port}...`));

0 commit comments

Comments
 (0)