grassrootsgrocery / admin-portal

GNU Affero General Public License v3.0
11 stars 6 forks source link

Grassroots Grocery Events Portal

Point of Contact

For inquiries about the project or if you want to contribute as a developer, contact Dan Zauderer (dan@grassrootsgrocery.org) or Matt Sahn (matt@grassrootsgrocery.org). We are always looking for people that want to contribute their coding skills to this project!

Project Info

Grassroots Grocery is an organization founded by Dan Zauderer that delivers free produce to areas that need it in NYC. The organization hosts produce-packing events every Saturday, where volunteers come to a designated location and load produce into their personal vehicles. Volunteers then drive the vehicles to various community locations around NYC.

Currently, Grassroot's technical infrastructure for managing all their volunteers' and coordinators' data is a collection of different services wired together (main ones are Airtable, Twilio, Make, Front, and Jotform). Grassroots Grocery uses these technologies to keep track of people who register for events, schedule events, send text messages to people to remind them of events, and much more. Because the system is so ad hoc, it is very difficult for anyone besides Dan to use it. This project is to build a web app that brings together all of these services together into an easy-to-use portal that will allow multiple people to organize and run events. Ultimately, it will allow for scaling out the operations of Grassroots Grocery to more sites around NYC and beyond.

This application started as a semester-long project by students at University of Maryland as part of Hack4Impact.

The stack

Here is a current diagram of what the infrastructure for the project currently looks like.

.env files

There are two .env files in the project: .env in the root of the project and client/.env.

API Key Access & Management

How to get the API key for development

Airtable

Make

Using the API keys

Backend

Access your API keys and env variables on the backend like this:

process.env.MAKE_API_KEY
process.env.AIRTABLE_API_KEY
process.env.AIRTABLE_BASE_ID_PROD
process.env.AIRTABLE_BASE_ID_DEV

Running the dev server

  1. Clone the repo and cd into it

  2. Run npm ci in the root directory

  3. Run npm ci in the client/ directory

  4. Follow the steps in the section above to set up your API keys (API Key Access & Management)

  5. Add to the root .env file:

    JWT_SECRET=96024
    TODAY=<YYYY-MM-DD>
    NODE_ENV=development
    AIRTABLE_BASE_ID_DEV=<dev base ID>
    AIRTABLE_API_KEY=<your airtable API key>

    i.e. TODAY=2023-02-01

  6. Add VITE_SERVER_URL='http://localhost:5000'to your client/.env file

  7. Run npm run dev in the root directory

  8. Navigate to localhost:5173 in your browser

  9. Log in with the username and password that was provided to you in your onboarding email (or ask Dan or Matt)

Hosting

We are currently using Railway as our hosting solution. The application is hosted at https://portal.grassrootsgrocery.org/, though it requires having a credential to log in.

Tech Resources

Data fetching and React Query

In order to simplify our calls to the Airtable API, we decided to use React Query to handle the data fetching layer of our app. While introducing libraries to the codebase does add complexity, we believe that the tradeoff in this case is worth it due to the benefits of caching, client/server synchronization, and state management that React Query provides. Here is a short primer.

Making a fetch call

The vanilla way to fetch data in React is usually something like this:

function MyComponent() {
  const [data, setData] = useState();
  const [loading, setLoading] = useState();
  const [error, setError] = useState();

  const fetchData = async (url) => {
    setLoading(true);
    try {
      const resp = await fetch(url);
      const data = await resp.json();
      setData(data);
    } catch (error) {
      setError(error);
    }
    setLoading(false);
  }
  useEffect(() => fetchData("https://someurl.com"), []);

}

Using React Query, the same fetch call would be written like this instead.

import { useQuery } from "react-query";

const { data, status, error } = useQuery(["thisIsTheQueryKey"], async () => {
    const resp = await fetch("https://someurl.com");
    return resp.json();
});

Let's break down the code above. The useQuery hook takes in an array of strings as its first argument. The elements of this array collectively make up the query key of this particular useQuery call. The second argument is our fetching function. useQuery returns an object that has a data attribute, which stores the result of the fetch, status, which stores the status of the fetch ("loading", "idle", "success", or "error"), and error, which stores errors thrown from the fetch. For the status variable, "loading" and "idle" are the same thing ("idle" has been removed in future versions of React Query).

So what's the big deal? Why is this better than the vanilla way of fetching? Aside from being shorter and more concise, React Query does a bunch of stuff under the hood for us that we would rather not have to think about (caching, deduping requests, refetching on error, etc.). One thing that we care about in particular is caching. Because React Query automatically caches the results of requests on the client, we can use the cache as a way to share the data we get back from requests throughout our application. This is where the query key comes into play. If you imagine the cache as a hash map, the query key is the key that lets you index into the map and get the cached data. After the first time the request above is made successfully, the cache looks like

const cache = {
  "thisIsTheQueryKey": //The data that was returned from the fetch
}

This means that subsequent calls to useQuery with the same query key will first read from the cache before making the request, which means that our data can be displayed instantly. This also means that if we have code that looks like this:

import { useQuery } from "react-query";

/*

What our cache looks like...

const cache = {
  "thisIsTheQueryKey": //The data that was returned from the fetch
}

*/

function ComponentA() {
  const { data, status, error } = useQuery(["thisIsTheQueryKey"], async () => {
      const resp = await fetch("https://someurl.com");
      return resp.json();
  });

}

// Somewhere else in our app...
function ComponentB() {
  const { data, status, error } = useQuery(["thisIsTheQueryKey"], async () => {
      const resp = await fetch("https://someurl.com");
      return resp.json();
  });
}

Both ComponentA and ComponentB read from and populate the same cache, which makes it really easy for us to share that data between them. Note that because the query key is the key for accessing fetched data in our cache, it should be unique to the fetch function. In other words, you should not have two useQuery calls that have the same query key, but different fetch functions.

More resources

Below are also linked some more resources about React Query for further edification.

Videos

Guides and Docs

TypeScript

TypeScript is safer than JavaScript, most UMD Hack4Impact teams use TypeScript, and TypeScript experience looks better on your resume than JavaScript experience. For these reasons, we've decided to use TypeScript in this project.

* VSCode tip: If you hover over a type, VSCode will show you the type. If you press Ctrl (Cmd on Mac?) while hovering, VSCode will show you more information about that type (such as its properties).

Styling

CSS

A few general principles on writing vanilla CSS.

  1. All of the colors used in the app are defined as CSS variables in App.css so use those when trying to color things.
  2. Write semantic HTML. Use things like section, nav, footer, ol, ul, etc. where they make sense.
  3. Dan expects to be able to use the portal on his phone, so try to make the pages responsive. This means that the font size adjust based on the screen size. Use things like clamp to assist with this.
  4. Favor rem instead of px, since rem is based off of font size.
  5. Scope your CSS to avoid your styles being incorrectly applied to other parts of the application. For example, instead of writing .logo, write .navbar .logo. This ensures that the styles are only applied to the elements with className="logo" who are also children of an element with className="navbar". This also can make your CSS more understandable. Writing .event-card .mid-section .date is clearer than just writing .date.
  6. Try to have your CSS rules follow the order in which the CSS elements appear in your markup, like in the example below.
function EventCard() {
  return (
    <li className="card">
      <div className="date">
      </div>

      <div className="middle-row">
        <div className="left">
          <div className="mid-section">
          </div>
        </div>
      </div>

      <div className="bottom-row">
        <div className="section">
          <div className="text-label">
          </div>
        </div>
      </div>
    </li>
  );
}
.card {

}
.card
.date {

}

.card
.middle-row {

}
.card
.middle-row
.left {

}

.card
.bottom-row {

}
.card
.bottom-row
.section {

}
.card
.bottom-row
.section
.text-lable {

}

Tailwind CSS

Because the tech lead on this project can't resist playing with new tech, we decided to use Tailwind CSS to aid with our styling. Tailwind was introduced because we wanted a bit more systematic approach to writing CSS. We also wanted something that would help us tackle responsiveness. However, it is not a requirement that things be styled using Tailwind. The codebase is currently a mix of components that are styled with Tailwind and components that are styled with vanilla CSS.

Tailwind VSCode Extension

This extension is called Tailwind CSS IntelliSense, and you should install it if you plan on using writing Tailwind in this project.

More resources