1.0.0 • Published 4 years ago

digital-motors-web-client v1.0.0

Weekly downloads
4
License
ISC
Repository
github
Last release
4 years ago

Web Client

Environment Variables

In order for the application to run you should provide the following environment variables

VariableDescriptionDefault ValueExample
PORTThe port the application runs on3000
BASE_URLURL for the running version of the app Used to proxy backend API linksNA"http://localhost"
CLOUDINARY_CLOUDNAMEThe Cloudinary space in which to grab imagesNA"digital-motors"
INVENTORY_API_URLURL for the backend inventory microserviceNA"https://inventory-service-dev-dm.herokuapp.com"
TENANT_API_URLURL for the backend tenant serviceNA"https://tenants-service-dev-dm.herokuapp.com"
LENDERS_API_URLURL for the backend lenders serviceNA"https://lenders-service-dev-dm.herokuapp.com"
INCENTIVES_API_URLURL for the backend incentives serviceNA"https://incentives-service-dev-dm.herokuapp.com"
USERS_API_URLURL for backend user serviceNA"https://users-service-dev-dm.herokuapp.com"
UTILS_API_URLURL for utilities backend serviceNA"https://utilities-service-dev-dm.herokuapp.com"
TENANT_SLUGURL of specific tenant (Dev Only)NAkarma.dev.digitalmotors.com
GOOGLE_MAPS_API_KEYAPI key for Google mapsNA
LOCAL_PRODIf running prod build locally, set this to trueNA
FEATURE_IS_SALES_TAX_ENABLEDWhether or not to show tax rates in the riderfalse
FEATURE_IS_DEALSHEET_FINANCE_OFFERS_ENABLEDWhether or not to show finance offers on the dealsheetfalse

To add all of these variables in development you can add a .env file and follow the file format from dotenv

Login with SiteGuard

Currently, when you first get the app up and running, you'll be presented with a login screen. It'll look something like this:

sign in screen

Basically what this is is a guard to keep people from snooping around the app while it's under development. Eventually, after we launch to real users this should be removed.

For now, in order to get into the app you can sign in with the following credentials.

  • Username: digitalmotors
  • Password: d1g1talm0t0rs

After logging in you should be directed into the actual app.

NOTE: This is not the same as the user authentication flow, that is separate from the site-guard.

File System

Here's a quick overview on the file system and how the project is arranged.

components folder

This folder will contain pretty much everything that's a reusable React component. Within components, the goal is to keep the file structure somewhat flat—ideally each folder directly inside of components should export an actual component (rather than making groupings of components in subfolders like form/Input, form/Select, so on and so forth).

The main reason for this is to make it easier to see what components are available for use by scanning the components directory. It also makes sure import paths stay clean(er).

appPages folder

This folder houses all of the components that directly map to NextJS' pages directory. There's a few reasons that we need to have this appPages directory in addition to the pages directory.

One reason is that NextJS uses the filesystem to create the URL structure. While we can map certain files to alternate routes, it's much easier to keep the code that's changing frequently (the appPages components) separate from the stuff that shouldn't be changing nearly as much (filenames / routes in pages). This makes it really easy to migrate to new routes in pages or rename components as we need to.

A more important reason is that NextJS treats everything in the pages directory as a route. This means that all files inside of a pages folder will be processed and bundled when NextJS is doing it's build. Oftentimes it's advantageous to split out parts of a page for abstraction purposes, and it's nice to keep all the components that are specific to the page inside the same folder as the main page. Additionally, it's nice to colocate tests, mocks, and a bunch of other stuff. This keeps development overhead a bit smaller since we don't need to go into nearly as many folders as we would if our related components were spread all over the codebase.

The problem is that we don't want our tests and "subcomponents" being bundled into standalone pages. 😱

This is why all pages should go in the appPages directory. The format between appPages and pages would look something like this

appPages/
  |- ExamplePage
      |- __mocks__
      |- __tests__
      |- utils/
          |- someUtilityFunction.ts
      |- components/
          |- SomeSubcomponent.tsx
      |- index.ts
      |- ExamplePage.tsx
pages/
  |- example/
      |- index.ts

And then in pages/example/index all we would need to do is this:

export { default } from '~appPages/ExamplePage';

This makes sure that the only thing NextJS builds is the default export from ExamplePage.

Handling Subcomponents and "single-use" components

Sometimes you might want to create a new component for the purpose of abstraction rather than reuse. For example, let's take this ExamplePage page component—it's got a sidebar with a ton of UI, a list of results, as well as its own state management.

Something like this:

example page

It doesn't make much sense to write the entire ExamplePage component in a single file—it would be massive and hard to trace what's going on! You'd probably split out a component for the sidebar, a component for each list item, perhaps a component for the entire list. However, these specific components aren't necessarily reused—unlike common UI components, they're inherently tied to the ExamplePage layout and business requirements.

In this case the file structure inside components has mostly been to keep subcomponents in the same folder as the main component. So for our ExamplePage component the folder inside appPages might look something like this:

appPages/ExamplePage
  |- __mocks__
  |- __tests__
  |- components/
      |- Sidebar.tsx
      |- List.tsx
      |- ListItem.tsx
  |- ExamplePage.tsx (main component)
  |- index.ts

By leaving the Sidebar, List, and ListItem inside ExamplePage/components this lets you break apart components where the abstraction feels most appropriate without having tons of these "single-use subcomponents" scattered all over the codebase. In addition, it makes sure that the stuff in appPages is limited to NextJS pages and the stuff in components is limited to reusable building blocks.

A nice side-effect is that if you're refactoring an individual page everything you need that's unique to that page should be inside the folder, so you can get around all the code you need somewhat fluidly.

If we see lots of duplicated code across multiple "subcomponents" (i.e. a Sidebar inside of ExamplePage and a Sidebar inside of AnotherExamplePage), that's the time when we can create a new component at the top-level of components—this helps us to create a meaningful abstraction once we know what reuse we want to abstract rather than repurposing a "single-use" component to suddenly handle multiple cases.

_NOTE: in the past the team had taken a slightly different approach to file structure where all of the subcomponents were in the folder at the same level (for example, ExamplePage/Sidebar) rather than a nested ExamplePage/components/Sidebar format. If you see this pattern feel free to move the files around into a nested components folder. Yay for incrementally improving our codebases!

context folder

The context folder holds the state stores for global app state. It shouldn't have a ton of items in there—just because something is using the React's context API doesn't necessarily mean it belongs in here. The files inside of context should be reserved for state management at the App level.

Currently each item inside context does a few things. Here's a sample context, let's call it ExampleContext

import { createContext, useContext } from 'react';

export const ExampleContext = createContext(null);

export const ExampleContextProvider = props => {
  const [state, setState] = useState(props.initialValue);

  // Typically you'd want to memoize this somehow, either make the
  // ExampleContextProvider a class component or useMemo.
  // Basically make sure that the value passed into ExampleContext.Provider
  // doesn't rerender a ton.
  // Here's a link to the React Context docs about this:
  // https://reactjs.org/docs/context.html#caveats
  const value = { state, setState };

  return (
    <ExampleContext.Provider value={}>{props.children}</ExampleContext.Provider>
  );
};

// Custom React hook in order to "read" from the context
export const useExampleContext = () => {
  const ctx = useContext(ExampleContext);

  // Sometimes add a check here to help prevent dev error,
  // for example make sure this hook is always used inside
  // ExampleContextProvider
  if (!ctx) {
    throw new Error(
      'useExampleContext should be used within ExampleContextProvider'
    );
  }

  return ctx;
};

Most of these global contexts will be implemented inside the App component.

api folder

The api folder holds the majority of the code interacting with the back-end APIs. There's a single folder per microservice, and then each different call is set up as its own function / folder within the respective microservice folders.

Typically, these API-calling functions do something along the lines of fetch data and do some light formatting / normalization to make stuff a little easier to work with once we're in the app.

layouts folder

The layouts folder should a few global layouts. These are layouts that are specific to a group of pages—but the layout is only mounted once, in App. The reason for this is to keep the a layout from unnecessarily mounting / unmounting during route transitions (when that happens you sometimes see a flash between the unmount / mount transition, as well as some other jank).

The layouts are rendered & configured inside of App. Each individual page component has the ability to configure and choose which layout it wants (at the time of writing there's only 1 layout, but it's shared by 5 pages and the individual pages slightly configure the layout).

pages folder

The pages folder is something that's specific to NextJS. NextJS takes everything in the pages directory and turns it into a server-rendered route.

Once again, one thing to remember is that every file inside of pages is bundled with the pages bundles. If your page component has stuff that shouldn't be bundled and sent to the user (for example, __tests__ or __mocks__) it's a good idea to keep the file in the pages directory limited to a simple export of another component. This makes sure that only the page component is built into the page bundle, rather than innadvertedly including the tests, mocks, and other stuff.

// pages/test.tsx

// TestPage has all the actual code, tests, and mocks
// for test.tsx
export { default } from '~appPages/TestPage';

lib folder

The lib folder serves a folder for utilities. Any common, reusable functions can go inn here, as well as custom React hooks can be dropped in here.

styles folder

Since we're using CSS-in-JS (see styling section) to apply our CSS styles, the styles folder holds a bunch of stuff that's helpful for styling. Media queries, CSS "mixins", and CSS "variables/constants" (from the design system) should all be in here.

🚨 Just remember that if you're using styled components, make sure to import styled from @digitalmotors/ui.theme instead of from @emotion/styled! This injects the theme interface so that TS can know what the theme should look like. If you import from @emotion/styled you might end up with some prretty cryptic TypeScript errors!

Accessibility

Screen-reader-only text

Many times for icon buttons you'll need some invisible text to make the action item accessible for screenreaders. This makes it so that the screenreader can read an action item, but sighted users only see the icon.

You can make screenreader-only text by using the size prop on the <Text /> component

<Text visuallyHidden>my screenreader text here</Text>

Styling 💅

Current styling in the web-client relies on a few libraries as well as some custom CSS-in-JS. Here's a bit of details about how to get things looking pretty

Emotion

The bulk of the CSS in this app is mostly done via Emotion, which is a CSS-in-JS library.

Here's a couple things we get for "free" out of the box with Emotion / CSS-in-JS

  • The only CSS rendered in the DOM is the CSS for HTML elements that are currently in the DOM. This means that at any given moment we're rendering exactly what we need, nothing more. This also helps with initial load times as the app grows since we don't have to worry about code-splitting our CSS bundles per route—Emotion does that for us!
  • Critical CSS (CSS needed for the first view of server-rendered HTML) is sent in the initial server-rendered HTML. All other CSS is not—it's rendered at runtime! This comes out of the box via the @emotion/ssr package. This helps the initial performance and also helps with avoiding a flash of unstyled content.
  • Theming is super easy, we can generate themes for Emotion and have the theme-specific colors, fonts, etc applied at runtime. Since this app has to be multi-tenant this helps with allowing each tenant to configure the theme however they want.

Emotion has a few ways of applying styles to a component, we'll touch on a couple of the most common ways here.

Styled Components

Emotion's styled API allows you to create new React components that already have the styles applied! In addition, you get to write your CSS in a template literal that looks very similar to actual CSS / SCSS. Here's a quick example of what styling a div would look like using the styled API

import styled from './styles/styled';

const StyledDiv = styled.div`
  background: blue;

  color: ${props => props.textColor};
`;

function MyReactComponent() {
  return <StyledDiv textColor="white">some content</StyledDiv>;
}

Anytime you render StyledDiv in a React component, it will automatically have the background: blue styles applied! You'll also notice that you can create styles based off of the styled component's props by creating a function inside of the ${}.

In addition, you can also pass another React component into the styled function!

const StyledDiv = styled(AnotherComponent)`
  background: blue;

  color: ${props => props.textColor};
`;

function MyReactComponent() {
  return <StyledDiv textColor="white">some content</StyledDiv>;
}

Under the hood, this composes the custom CSS you just added to the styled component along with whatever styles were already on the component.

⚠️️️️️ Because we're working with TypeScript, the way that we import styled will look slightly different than the examples in the Emotion docs. ️⚠️

You'll need to import from ./styles/styled instead of from @emotion/styled. This makes sure that the correct type definitions are added for the theme prop (automatically added to the styled component by Emotion).

If you want to read more on styled-components in Emotion, check out the docs!

The css prop

Another way that you can apply CSS styles in Emotion is thru the css prop. This is really nice for small one-off styles or situations where you need to create some custom styles but you don't want to go create another styled component (and name it, so on so forth). One of the other nice little benefits of the css prop is you still see the HTML tag inside of your JSX if that's something you prefer.

Using the css prop looks something like this

import { css, jsx } from '@emotion/core';

function MyReactComponent() {
  return (
    <div
      css={css`
        background: blue;
        color: white;
      `}
    >
      some content
    </div>
  );
}

There are some caveats with the css prop, the biggest being that you don't automatically have access to the theme prop like you do with styled. However, it's useful to know that both APIs exist—you might find yourself using a mix of the two based on what feels cleanest in the moment.

Reactstrap

In addition to using emotion we're also using Reactstrap and Bootstrap to make scaffolding some pages a little faster.

For the most part, the components from ReactStrap should be wrapped in new components that apply unique styles. Since the designs aren't 1:1 to bootstrap components, this allows some of the speed of development from using ReactStrap (we're not building modals / dropdowns / etc from scratch and dealing with all the weird edge cases) while also allowing us to use the design system without having to override all over the place.

Note: Eventually it might be nice to strip out Bootstrap, or at least ship a slimmed down version of it. Since we're wrapping the Reactstrap components it'll make it easier to strip out the Reactstrap components if we need to.

Overriding Bootstrap styles

Many times Bootstrap will chain multiple classes together into a string for certain interaction states. These can get a little difficult to override with custom styles without resorting to using !important. 😱

As a way to override Bootstrap you can leverage the #body id selector. This id has been set on the root <body> tag and allows you to increase your selector specificity to be higher than Bootstrap's.

For example, consider the following CSS selector from Bootstrap along with its Emotion override:

// Bootstrap selector
btn-outline-secondary:not(:disabled):not(.disabled):active {
  // default bootstrap CSS
}
const Btn =
  styled.button`
  /* The ` &
  ` selector refers to the auto-generated
   * Emotion classname
   */
  #body &:active {
    /* Override styles go here */
  }
`;

Testing

Testing styled components

References / Links

Internationalization (i18n)