diff --git a/.babelrc b/.babelrc deleted file mode 100644 index c14b282..0000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["react-app"] -} diff --git a/.circleci/config.yml b/.circleci/config.yml index a69a645..fd883a9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: test: docker: - - image: node:9.3.0 + - image: node:11 steps: - checkout @@ -36,7 +36,7 @@ jobs: - run: name: Code Style Lint - command: yarn check-style + command: yarn check-fmt - run: name: Run Tests @@ -69,37 +69,6 @@ jobs: FOLDER_NAME=$([ -z "$CIRCLE_TAG" ] && echo "$CIRCLE_BRANCH" || echo "$CIRCLE_TAG") aws s3 cp ./forte-web.tar.gz "s3://forte-web-artifacts/${FOLDER_NAME}/forte-web.tar.gz" - deploy_surge: - docker: - - image: node:9.3.0 - - steps: - - checkout - - - restore_cache: - keys: - - dependencies-{{ checksum "package.json" }} - # fallback to using the latest cache if no exact match is found - - dependencies- - - - run: - name: Install Deps - command: yarn install - - - run: - name: Build - environment: - REACT_APP_MOCK_RESOLVER: true - command: yarn build - - - run: - name: Copy Index File - command: cp build/index.html build/200.html - - - run: - name: Deploy to Surge - command: yarn surge --project ./build --domain forte.surge.sh - workflows: version: 2 test: diff --git a/config/storybook/addons.js b/.storybook/addons.js similarity index 100% rename from config/storybook/addons.js rename to .storybook/addons.js diff --git a/.storybook/config.js b/.storybook/config.js new file mode 100644 index 0000000..c0543c0 --- /dev/null +++ b/.storybook/config.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { configure, addDecorator } from '@storybook/react'; +import { RootThemeProvider } from '../src/components/App/RootThemeProvider'; +import './styles.css'; + +const themeDecorator = story => ( + {story()} +); +addDecorator(themeDecorator); + +// This condition actually should detect if it's an Node environment +// https://stackoverflow.com/questions/38332094/how-can-i-mock-webpacks-require-context-in-jest/42191018#42191018 +if (typeof require.context === 'undefined') { + const fs = require('fs'); + const path = require('path'); + + require.context = ( + base = '.', + scanSubDirectories = false, + regularExpression = /\.js$/ + ) => { + const files = {}; + + function readDirectory(directory) { + fs.readdirSync(directory).forEach(file => { + const fullPath = path.resolve(directory, file); + + if (fs.statSync(fullPath).isDirectory()) { + if (scanSubDirectories) readDirectory(fullPath); + + return; + } + + if (!regularExpression.test(fullPath)) return; + + files[fullPath] = true; + }); + } + + readDirectory(path.resolve(__dirname, base)); + + function Module(file) { + return require(file); + } + + Module.keys = () => Object.keys(files); + + return Module; + }; +} + +const req = require.context('../src', true, /\.stories\.tsx$/); + +configure(() => { + req.keys().forEach(filename => req(filename)); +}, module); diff --git a/config/storybook/styles.css b/.storybook/styles.css similarity index 100% rename from config/storybook/styles.css rename to .storybook/styles.css diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 889be75..e34c571 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,58 +1,91 @@ # Contributing to forte-music/web -Thanks for taking the time to contribute to forte. Here are some guidelines to follow when contributing. +Thanks for taking the time to contribute to forte. Here are some guidelines +to follow when contributing. -Some of the code in this project hasn't been refactored to follow these guidelines yet. +Some of the code in this project hasn't been refactored to follow these +guidelines yet. ## CSS -For the most part, CSS is written using [styled-components]. Currently, they are scattered near the presentational components they are used in, in `src/components/styled` and in `src/components`. Going forward, mixins should be placed in `src/styled-mixins`, shared styled components in `src/components/styled`, and unshared styled components, near where they are used. +For the most part, CSS is written using [styled-components]. Currently, they +are scattered near the presentational components they are used in, in +`src/components/styled` and in `src/components`. Going forward, mixins should +be placed in `src/styled-mixins`, shared styled components in +`src/components/styled`, and unshared styled components, near where they are +used. -Avoid using theming unless needed. It adds un-needed complexity for most use cases. If you are using the same component with slightly different styles in two different places, then use themeing. +Avoid using theming unless needed. It adds un-needed complexity for most use +cases. If you are using the same component with slightly different styles in +two different places, then use themeing. ## Redux -The [render-props] pattern is used to decouple data providing components with components consuming data. We use a wrapper around `connect` (a Higher Order Component from `react-redux`) to use the render props pattern with redux. This wrapper is called `createReduxComponent` and can be found in `src/redux/render.tsx`. + +The [render-props] pattern is used to decouple data providing components with +components consuming data. We use a wrapper around `connect` (a Higher Order +Component from `react-redux`) to use the render props pattern with redux. +This wrapper is called `createReduxComponent` and can be found in +`src/redux/render.tsx`. ## Component Folder Structure + There are a few different types of components. * Presentational Components - Stateless components which are primarily concerned with how things look. State and actions are passed as props and prop callbacks respectively. These are usually functional components. Usually placed in `src/components/`. + Stateless components which are primarily concerned with how things look. + State and actions are passed as props and prop callbacks respectively. These + are usually functional components. Usually placed in `src/components/`. * Enhancer Components - Often handle data fetching and state storage concerns. Usually stored next to the container component they are used in (for example `src/components/AlbumsContainer/enhancers/redux.ts`). + Often handle data fetching and state storage concerns. Usually stored next to the container + component they are used in (for example + `src/components/AlbumsContainer/enhancers/redux.ts`). * Container Components - Made up by composing many enhancers and on or more presentational components. Found in folders ending with `Container` (for example `src/components/AlbumsContainer/index.tsx`). + Made up by composing many enhancers and on or more presentational + components. Found in folders ending with `Container` (for example + `src/components/AlbumsContainer/index.tsx`). [Here is some more information about the presentational and container component pattern.][container-component] ## Code Style -[Prettier] is used to keep our code formatted consistently. Run `yarn fix-style` to reformat code to follow adhere to Prettier's style. Run `yarn check-style` to see which files don't adhere to Prettier's style. +[Prettier] is used to keep our code formatted consistently. Code is formatted +in a precommit hook. Run `yarn fmt` to reformat code manually. ## Storybook -[Storybook] is used for testing react components outside of the complete application. Run `yarn storybook` to start it with mock data. Files ending with `.stories.tsx` are picked up storybook. A test ensures that all stories render without crashing. +[Storybook] is used for testing react components outside of the complete +application. Run `yarn storybook` to start it with mock data. Files ending +with `.stories.tsx` are picked up storybook. A test ensures that all stories +render without crashing. ## Tests -Tests are run using [Jest]. Run `yarn test` to start jest. Tests should placed in files ending with `.test.ts` or `.test.tsx`. +Tests are run using [Jest]. Run `yarn test` to start jest. Tests should +placed in files ending with `.test.ts` or `.test.tsx`. ## Lints -[TSLint] is used for linting code. Run `yarn tslint` to run tslint on this project. +[TSLint] is used for linting code. Run `yarn tslint` to run tslint on this +project. ## CircleCI Build -CircleCI does a number of [tasks] after your code is pushed. Make sure your code passes before submitting a PR. Many of the same tests run in CircleCI can be run with `yarn check-all`. +CircleCI does a number of [tasks] after your code is pushed. Make sure your +code passes before submitting a PR. Many of the same tests run in CircleCI +can be run with `yarn check-all`. ## Contributor Agreement -By contributing code to our project in any form, including sending a pull request via Github, a code fragment or patch via private email or public discussion groups, you agree to release your code under the terms of the license that you can find in the LICENSE.md file included in the forte-music/web source distribution. +By contributing code to our project in any form, including sending a pull +request via Github, a code fragment or patch via private email or public +discussion groups, you agree to release your code under the terms of the +license that you can find in the LICENSE.md file included in the +forte-music/web source distribution. [jest]: https://jestjs.io/ [prettier]: https://prettier.io/ diff --git a/README.md b/README.md index 81b6b27..a661a48 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,40 @@ # web -[![Build Status][build-status-image]][build-status] +A web interface for streaming music from forte. -[Check out the demo site!][demo-site] +[![Build Status][build-status-image]][build-status] ## Quickstart yarn query-codegen && yarn start-mock -## Build Configuration - -This project was bootstrapped with [create-react-app]. We've since ejected, but still use many of its features. - ## Generated Files -[GraphQL] is used to talk to the backend. Given a schema and a graphql request string, we can compute the response type and provide it to typescript so typechecking code around API calls works. - -To generate typing information for responses which typescript understands, use `yarn query-codegen`. Re-run this command every time a query or the schema changes and after pulling new versions from the remote. These files are stored in a sibling folder named `__generated__` to the file the query is defined in. The files in `__generated__` are ignored by git. - -The `query-codegen` script is run before the `build` and `check-all` scripts. - -## Mock Backend - -A few things change when the application is run in an environment with the `REACT_APP_MOCK_RESOLVER` variable set to a truthy value. - -First, a mock [GraphQL] resolver is used instead of a resolver which queries a remote endpoint. The graphql resolver is provided by the [`@forte-music/mock`][forte-music/mock] npm package with data from the [`@forte-music/schema`][forte-music/schema] npm package. [The mock data can be found in the toml files here.][mock] - -Second, instead of fetching audio files from the remote server, a fixed set of audio files is used instead. Each song in the mock data maps to an audio file based on the identifier of the mock song's identifier. Multiple songs may share the same underlying audio file. +[GraphQL] is used to talk to the backend. Given a schema and a graphql +request string, we can compute the response type and provide it to typescript +so typechecking code around API calls works. -These features allow developing the frontend independently of the backend. +To generate typing information for responses which typescript understands, +use `yarn query-codegen`. Re-run this command every time a query or the +schema changes and after pulling new versions from the remote. Generated +files are stored in a sibling folder named `__generated__` to the file the +query is defined in. Files in `__generated__` are ignored by git. -The `start-mock` and `storybook` scripts enable mock behavior for their corresponding actions. +## Backend -## External Server +By default (`yarn start`) this application connects to a backend hosted at +`localhost:8080`. Configure this by changing the proxy key of `package.json`. +See the [create react app proxy guide][proxy-guide] for more. -To use an external graphql resolver, run `yarn start` instead of `yarn start-mock` and configure the [Create React App Proxy][proxy-guide] to proxy requests to your server. +There's a mock backend which can be enabled by using `yarn start-mock`. When +enabled, a mock graphql resolver will run inside the dev server. A playground +can be found at `localhost:3000/graphql`. The mock resolver is provided by +[`@forte-music/mock`][forte-music/mock]. The mock backend even configures the +app to use a set of royalty free tracks so features around playback can be +tested. [graphql]: https://graphql.org/ -[demo-site]: https://forte.surge.sh/ -[proxy-guide]: https://github.com/facebook/create-react-app/blob/cb1608b3e02e0eef5fd350f6e4cf5ce32bdfc215/packages/react-scripts/template/README.md#proxying-api-requests-in-development +[proxy-guide]: https://facebook.github.io/create-react-app/docs/proxying-api-requests-in-development [build-status-image]: https://img.shields.io/circleci/project/github/forte-music/web/master.svg [build-status]: https://circleci.com/gh/forte-music/web [forte-music/mock]: https://github.com/forte-music/schema/tree/master/packages/mock -[forte-music/schema]: https://github.com/forte-music/schema/tree/master/packages/schema -[mock]: https://github.com/forte-music/schema/tree/master/packages/schema/fixtures -[create-react-app]: https://github.com/facebook/create-react-app diff --git a/config/aliases.js b/config/aliases.js deleted file mode 100644 index 3fbf550..0000000 --- a/config/aliases.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - // Support React Native Web - // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ - 'react-native': 'react-native-web', -}; diff --git a/config/env.js b/config/env.js deleted file mode 100644 index 8b39b6c..0000000 --- a/config/env.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const paths = require('./paths'); - -// Make sure that including paths.js after env.js will read .env variables. -delete require.cache[require.resolve('./paths')]; - -const NODE_ENV = process.env.NODE_ENV; -if (!NODE_ENV) { - throw new Error( - 'The NODE_ENV environment variable is required but was not specified.' - ); -} - -// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use -var dotenvFiles = [ - `${paths.dotenv}.${NODE_ENV}.local`, - `${paths.dotenv}.${NODE_ENV}`, - // Don't include `.env.local` for `test` environment - // since normally you expect tests to produce the same - // results for everyone - NODE_ENV !== 'test' && `${paths.dotenv}.local`, - paths.dotenv, -].filter(Boolean); - -// Load environment variables from .env* files. Suppress warnings using silent -// if this file is missing. dotenv will never modify any environment variables -// that have already been set. -// https://github.com/motdotla/dotenv -dotenvFiles.forEach(dotenvFile => { - if (fs.existsSync(dotenvFile)) { - require('dotenv').config({ - path: dotenvFile, - }); - } -}); - -// We support resolving modules according to `NODE_PATH`. -// This lets you use absolute paths in imports inside large monorepos: -// https://github.com/facebookincubator/create-react-app/issues/253. -// It works similar to `NODE_PATH` in Node itself: -// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders -// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. -// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. -// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 -// We also resolve them to make sure all tools using them work consistently. -const appDirectory = fs.realpathSync(process.cwd()); -process.env.NODE_PATH = (process.env.NODE_PATH || '') - .split(path.delimiter) - .filter(folder => folder && !path.isAbsolute(folder)) - .map(folder => path.resolve(appDirectory, folder)) - .join(path.delimiter); - -// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be -// injected into the application via DefinePlugin in Webpack configuration. -const REACT_APP = /^REACT_APP_/i; - -function getClientEnvironment(publicUrl) { - const raw = Object.keys(process.env) - .filter(key => REACT_APP.test(key)) - .reduce( - (env, key) => { - env[key] = process.env[key]; - return env; - }, - { - // Useful for determining whether we’re running in production mode. - // Most importantly, it switches React into the correct mode. - NODE_ENV: process.env.NODE_ENV || 'development', - // Useful for resolving the correct path to static assets in `public`. - // For example, . - // This should only be used as an escape hatch. Normally you would put - // images into the `src` and `import` them in code to get their paths. - PUBLIC_URL: publicUrl, - } - ); - // Stringify all values so we can feed into Webpack DefinePlugin - const stringified = { - 'process.env': Object.keys(raw).reduce((env, key) => { - env[key] = JSON.stringify(raw[key]); - return env; - }, {}), - }; - - return { raw, stringified }; -} - -module.exports = getClientEnvironment; diff --git a/config/jest/cssTransform.js b/config/jest/cssTransform.js deleted file mode 100644 index f1534f6..0000000 --- a/config/jest/cssTransform.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -// This is a custom Jest transformer turning style imports into empty objects. -// http://facebook.github.io/jest/docs/tutorial-webpack.html - -module.exports = { - process() { - return 'module.exports = {};'; - }, - getCacheKey() { - // The output is always the same. - return 'cssTransform'; - }, -}; diff --git a/config/jest/fileTransform.js b/config/jest/fileTransform.js deleted file mode 100644 index ffce0da..0000000 --- a/config/jest/fileTransform.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const path = require('path'); - -// This is a custom Jest transformer turning file imports into filenames. -// http://facebook.github.io/jest/docs/tutorial-webpack.html - -module.exports = { - process(src, filename) { - return `module.exports = ${JSON.stringify(path.basename(filename))};`; - }, -}; diff --git a/config/paths.js b/config/paths.js deleted file mode 100644 index 1df7516..0000000 --- a/config/paths.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs'); -const url = require('url'); - -// Make sure any symlinks in the project folder are resolved: -// https://github.com/facebookincubator/create-react-app/issues/637 -const appDirectory = fs.realpathSync(process.cwd()); -const resolveApp = relativePath => path.resolve(appDirectory, relativePath); - -const envPublicUrl = process.env.PUBLIC_URL; - -function ensureSlash(path, needsSlash) { - const hasSlash = path.endsWith('/'); - if (hasSlash && !needsSlash) { - return path.substr(path, path.length - 1); - } else if (!hasSlash && needsSlash) { - return `${path}/`; - } else { - return path; - } -} - -const getPublicUrl = appPackageJson => - envPublicUrl || require(appPackageJson).homepage; - -// We use `PUBLIC_URL` environment variable or "homepage" field to infer -// "public path" at which the app is served. -// Webpack needs to know it to put the right