[[TOC]]
Share your drink recipes and review others! Drinks is a full stack app for viewing and reviewing drink recipes! It is built with modern web technologies like React, Typescript, Tailwind and GraphQL.
- Clone the repo
- Run
npm install
in the root directory to install dependencies for the frontend and backend
- Spin up a postgres database
- Mac
- I use Postgres.app to host my database and Postico to interact with it
- You can also use Homebrew
- Or Docker
- Mac
- Create a
.env
file in the/server/prisma
directory - Add the database url to the
.env
fileDATABASE_URL="postgresql://<username>:<password>@<host>:<port>/<database>"
- Run
npm prisma:reset
in the/server
directory to create the database schema and seed it with mock data - Your backend is now ready to go!
- Run
npx prisma migrate dev
in the/server
directory to create the new database schema
To generate our seed data we use the TheCocktailDB API.
Ingredients data was downloaded from TheCocktailDB and manually cleaned and prepared for use in server/src/prisma/seed.ts
.
All drinks data was downloaded from a Kaggle dataset.
The data was manually cleaned,
then converted to JSON using server/src/lib/csvToJSON.ts
.
Furthermore it was cleaned and prepared with server/src/lib/createDrinksSeed.ts
.
These programs can be run with npm run run:drinksToJSON
and npm run run:drinksToSeed
respectively within the server/
folder.
npm run dev
runs the app and server in development modenpm run start
runs the app and server in production modenpm run test
runs tests for the app and server- Prefix
start
withserver:
to run only the server - Prefix all commands with
app:
to run only the app
More specific commands you find by checking out the subrepos /app
and /server
app/
contains web app codeindex.html
configure metadata here- is technically the main entry point for the application
cypress/
contains cypress tests and confige2e/
contains end-to-end tests
public/
contains public assetssrc/
contains the main code for the applicationassets/
contains private assets like images and fontscomponents/
contains components our team has writtenui/
contains shadcn/ui components
lib/
contains different utilities- Basically "etc." but for code reused throughout the application
pages/
define separate pages in the application- Importantly have the responsibility of data handling and conditional rendering
apolloClient.ts
is where we configure our Apollo clientApp.tsx
is where we apply our app layoutglobal.css
defines css variables and resets default stylingmain.tsx
is the main entry point for our application- Here providers, routing and styles are added to the application
router.tsx
is where we handle routing
data/
contains static mock dataserver/
contains server codeprisma/
migrations/
contains database migrations.env
contains environment variablesschema.prisma
defines the database schema
src/
contains the main code for the serverdata/
contains the data we "scraped" from TheCocktailDB and its transformationslib/
contains scripts for transforming data and creating seed dataprisma/
contains our seed scriptindex.ts
is the main entry point for the server- Here we define the server and connect it to the database
We use semantic versioning to keep track of our releases.
App versioning is unnecessary as the consumers of our app are not dependent on a specific version of our app.
Our server/backend is versioned as it is a public API that is consumed by our frontend.
Please refer to https://semver.org/ for more information.
Typescript is essential for our workflow as it provides us with type safety and a better development experience (DX).
ESlint is used for linting. Our configs lie in .eslintrc.cjs. To manually test the linting of the project run the command
npm run lint
Prettier is our choice of formatter. To manually format the project run
npm run format
in the app directory.
Our team prefers to use the VSCode Prettier extension with the "Format on save" option enabled.
That way it is practically impossible to push unformatted code.
.prettierrc
contains our configurations so everyone has the same formatting rules.
These are set to the defaults with the exception of enabling plugins.
The plugins we use are:
- "prettier-plugin-organize-imports"
- "prettier-plugin-tailwindcss"
These sort and clean up our imports and tailwind classes so to avoid meaningless merge conflicts.
Javascript frameworks are a staple of modern web development and React is our choice for its popularity and ease of use.
Vite is our required frontend build tool. No configuration is required outside of whats included in the repo.
Tailwind is our choice for in-line styling. It is popular for good reasons. The development experience (DX) it provides lets us focus on making a good website without having to bother with the intricacies of plain css.
Shadcn/ui or Shadui for short is a collection of re-usable components that you can copy and paste into your apps. This is not a component library in the sense that it is not a dependency.
We use shadcn/ui components as our base and build on top of the sensible defaults it provides.
For state management we use Apollo Local State Management. It is a simple and effective way to manage state in our application. It is also a good choice as we are already using Apollo for our GraphQL server and client.
We use Apollo Local State Management for storing the search paremeters and results, so searchPage persists when navigating to detailsPage and back.
React Router allows us to employ URL path routing in our Single Page Application (SPA)
- All pages are wrapped within
<App />
which contains layout elements like a universal header - Search Page (/): Users can search for and are presented with a myriad of cocktail recipes
- Details Page (/cocktail/:cocktailId): Displays information specific to the cocktailId parameter
- Error Page (*): Displays if a routing error occurs, for example navigating to an undefined page url
export const rootPath = "/";
export const appRouter = createHashRouter([
{
path: rootPath,
element: <App />,
children: [
{
path: rootPath,
element: <SearchPage />,
},
{
path: "details",
element: (
<>
<Outlet />
</>
),
children: [
{
path: ":drinkId",
element: <DetailsPage />,
},
],
},
],
},
]);
Cypress is our choice of testing library for its ease of use and its excellent E2E testing environment.
To open the Cypress testing app run npm run cypress:open
and it will automatically open.
From there on you can choose whether to run component or end-to-end (E2E) tests.
We do not write custom tests the shadcn/ui components as it is a stable "library" and we are not tinkering with its primitives.
We do however write tests for our custom components.
They are written in the same folder as the component they are testing and are named *.cy.tsx
.
Cypress is allowed for component tests.
Check thread @128 in Piazza π
Our most important tests are our e2e tests that ensures the app works as intended for our users.
To run the e2e tests both the app and server must be running.
Then run npm run cypress:open
in app/ and choose the e2e
folder.
Or run npm run test-e2e
in app/ to run all tests in the e2e
folder.
PostgresQL is all we need for our database. We could have chosen to make a project using a graph database, but for that choice to make sense it would require us to make for example a social media app with a lot of relations, like Twitter(X) or Facebook.
Prisma makes it incredibly easy to implement a database in our stack. It gives us type safety when defining our GraphQL resolvers and a simple way to interact with our database. It also provides us with a way to seed our database with mock data.
GraphQL is an effective and environmentally friendly way to query data from a server. We recognize that GraphQL might be overkill for our use case as we have a fairly simple relational database. However, we want to use GraphQL to learn. (And it is a requirement for the course π) A realistic application would either have a more complex database with more data or use a graph database like Neo4j to justify the use of GraphQL. Personally, I would use TRPC as our frontend and backend are written in Typescript, but GraphQL is more flexible as it is language agnostic. It is also more stable as it is a more mature technology with a large community.
Apollo is the easiest way to implement GraphQL in our stack. It is important not to overcomplicate our stack with too many libraries and Apollo is a good choice for that reason.
Apollo server is used to implement our GraphQL server.
Apollo client is used fetch data from our GraphQL server in the frontend.
We implemented the Shadcn/ui library, which is built on top of Radix, designed with accessibility in mind. For remaining gaps, we added aria-label to necessary components, such as interactive buttons.
We made the website keyboard-friendly so that users can navigate through the site using only the keyboard (tab, enter, space, and arrow keys).
The website also features dark mode, both to protect sensitive eyes and to reduce power consumption on screens. Speaking of which...
We chose to use low-resolution images from our dataset, considering that the quality from other images was unnecessary. This reduces the amount of data that needs to be transferred, which in turn reduces the energy consumption of the website. We could have gone further and converted them to WEBP instead of PNGs, but we decided that the time spent on that would be better spent on other features.
In our ApolloClient we implemented InMemoryCache which stores results from the GraphQL queries in memory. This can result in reduced network traffic, faster data responses and optimized resource usage. Which leads to reduced energy consumption.
We follow the conventional commits specifications described in https://www.conventionalcommits.org/en/v1.0.0/
more concise and web oriented document
We also use the conventional commits VSCode extension for ease of use
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
flowchart LR
DB_ACCESS --> Prisma
subgraph FULL_STACK
direction LR
DB[(PostgresDB)] --> Prisma
Prisma --> ApolloServer
subgraph BACKEND
DB
Prisma
ApolloServer
end
ApolloServer --> ApolloClient
subgraph FRONTEND
ApolloClient --> React-App
end
end
classDiagram
cocktailStore --> App
App --> SearchPage
SearchPage <--> cocktailStore
SearchPage --> DetailsPage
SearchPage --> ResultList
DetailsPage --> DrinkDetails
DetailsPage --> ReviewForm
DetailsPage --> ReviewList
ReviewList <..> ReviewListItem
class cocktailStore {
searchResults
setSearchResults()
clearSearchResults()
}
class App {
useQuery()
}
class SearchPage {
drink[] searchResults
String inputValue
}
class ResultList {
drink[]
}
class DetailsPage {
String drinkName
}
class DrinkDetails {
drinkDetails
}
class ReviewForm {
int rating
string text
}
class ReviewList {
drinkId
review
}
class ReviewListItem {
review
}