Luminskin.com prototype rewritten in NextJS
Installing dependencies
yarn
Setting up local ENV variables
cp .env.local-sample .env.local
Running locally
yarn dev
Building for prod locally
yarn build
yarn start
- [ ] Automate deployment of storybook
- [ ] Link
- [ ] Tag
- [ ] Radio button
- [ ] Toggle
- [x] Select
- [ ] Checkbox
- [ ] Popovers
- NextJS - React framework for server side rendering, routing and bundle splitting
- Vercel - Global deployments via CDN
- Styled System - For style theming
- Emotion - For css in js
- ChakraUI - While not a direct dependency we are using several of their components in ./src/design/components
- React Hook Form - Build forms with this :D.
Whenever a top level directory is added, please add an entry here denoting what should go in it.
Design system components go here, these components should be agnostic to this codebase and should able to be used as a "3rd party library". No files in here
should import anything from src/
This is where code domain specific code related to pangaea/lumin should go. It should be organized by domain (ex: checkout, cart, gifting).
-
<Module Name>/components
This is where the react components specific to a module in the codebase go.- For generic components there should be a
shared
module. - If you are making components as a way to break up a larger page please put them in
<Module Name>/components/<PAGE NAME>/*
. Keep in mind that it's preferable to create re-usable components instead of components specific to a single page.
- For generic components there should be a
-
<Module Name>/actionTypes.ts
This is where typescript types for redux actions (both the action and the action'stype
constant). This file should also export a variable<ModuleName>ActionTypes
which is a union of all action types
export const CHECKOUT_LOAD_ORDER = 'CHECKOUT_LOAD_ORDER'
export interface CheckoutLoadOrderAction {
type: typeof CHECKOUT_LOAD_ORDER
data: Order
}
export type CheckoutActionTypes = CheckoutLoadOrderAction
-
<Module Name>/actions.ts
This is whereredux
actions go as well as any function that does afetch
. Redux action types should be defined here asenum
-
<Module Name>/constants.ts
Constants specific to this domain. -
<Module Name>/functions.ts
This is where pure functions go (no side-effects) -
<Module Name>/reducer.ts
If a module needs to declare a redux reducer, it goes here. -
<Module Name>/routes.ts
This file should export an object similar to the following. -
<Module Name>/selectors.ts
Selectors should be exported here as named constants, starting withselect
(ex:selectCurrentOrder
import buildRoutes, { routeFromUrl } from '../shared/buildRoutes'
// Example for the products module
// The first parameter is the base value and reflects the grouping of this
// module's pages. It will be prefixed to all routes.
const productRoutes = buildRoutes('products', {
// Items represent a single page in the application.
list: 'list', // Points to '/products/list'
// If the value starts with a `/`, no prefix will take place.
alternate: '/products2' // Points to /products2
// For dynamic routes there's a helper function generator `routeFromUrl`.
detail: routeFromUrl<{ slug: string }>('detail/[slug]') // Will generate:
// detail() -> /products/detail/[slug]
// detail({slug: 'recovery-oil'}) -> /products/detail/recovery-oil
})
export default productRoutes
Defining redux store and root reducer should go here.
This directory follows the NextJS routing structure.
This is where basic helpers and utilities go, these should be generic and not overly specific to lumin (lodash or underscore like functions go in here).
Things that dont belong in src/utils/*.ts
- Constants - put them in
src/modules/<name>/constants.ts
and group with their specific module - Ajax/fetch functions - put them in
src/modules/<name>/actions.ts
and group with their specific module
This is where we expose values and functions to the window. No source code should ever import this file, it's purely for exposing to window.
File names should be camelCase
. If the file exports a React component or a class the filename should be TitleCase
matching the name of the default export.
We use prettier and eslint:recommended as a starting point. Everyone is free to make suggestions regarding which of the rules to change if they feel strongly about it but keep in mind that if the team as a whole cannot reach a decision, the existing rules will prevail.
The current configurations are:
To view examples and documentation of our design system run yarn storybook
.
All new components in src/design/components/
should have a file ComponentName.stories.mdx
created which will automatically add documentation for that component to storybook.
Follow the following techniques and examples.
Use fetch()
, we use the isomorphic-unfetch package, which acts as a polyfill for both nodejs and incompatible browsers.
import fetch from 'isomorphic-unfetch'
const req = await fetch('https://doit.com')
const resp = await req.json()
For linking to other pages in the codebase, import the relevant module's routes and use that with NextJS' <Link />
component.
import Link from 'next/link'
import routes from '../routes'
const Component = () => (
<p>
{/* NextJS uses `href` to crawl at build time and `as` to route at runtime. */}
Go to a product: <Link href={ routes.detail() } as="{ routes.detail('recovery-oil') }"><a>Recovery Oil</a></Link>
Go to the list: <Link href="{ routes.list() }"><a>Recovery Oil</a></Link>
</p>
)
TODO
TODO See the docs on env variables
NextJS utilizes ENV variables. For frontend variables prefix them with NEXT_PUBLIC
to be exposed to the FE bundle
All config values are centralized in ./src/core/config/config.ts, and new values should be added in here via ENV variables.
Accessing config values is through the config
function
import config from '../../core/config'
const BASE_URL = config('API_BASEURL')
Reducer's should be added to modules src/modules/<ModuleName>/reducer.ts
. They should export a named reducer function <ModuleName>Reducer.ts
All reducers are combined to the root reducer in ./src/redux/rootReducer.js. Make sure to update the RootState
type which is the composite of all the reducer's state types.
Actions should also be unioned on the type AppActionTypes
in ./src/redux/rootReducer.js
There are convenience hooks for using application typed selectors and dispatch
import { useTypedSelector, useTypedDispatch } from '../../redux/store'
// in your reac compoonent
const currentOrder = useTypedSelector(selectCheckoutCurrentOrder)
const dispatch = useTypedDispatch()
There are convenience types for creating actions and thunk actions inside the app.
import { ThunkAppActionCreator, AppActionCreator } from '../../redux/store'
export const loadOrder: AppActionCreator = (order: Order) => {
return {
type: CHECKOUT_LOAD_ORDER,
payload: order,
}
}
export const fetchOrder: ThunkAppActionCreator<Promise<Order>> = (
orderId: string
) => async (dispatch) => {
const order = await fetchOrder()
dispatch({
type: CHECKOUT_LOAD_ORDER,
payload: order,
})
}
Right now all of our custom defined error live in ./src/core/errors.ts. These error should remain generic and represent things that should break out of the User Experience (show an error page or boundry).
The our handling logic lives in ./src/pages/_ErrorBoundary.tsx this page handles forwarding error to our error reporting service and also displaying errors to the user.
Throwing errors in async functions
For our ErrorBoundary to properly catch an error it must be throw in the context of a React life cycle event.
Using const [, setError] = useState()
is an easy way to ensure this.
const [, setError] = useState()
useEffect(() => {
dispatch(fetchOrder(orderId)).catch(() =>
setError(() => {
throw new NotFoundError('Your order was not found')
})
)
}, [dispatch, orderId])