Skip to content
This repository has been archived by the owner on Apr 5, 2024. It is now read-only.

Commit

Permalink
Merge branch 'master' into uneject
Browse files Browse the repository at this point in the history
  • Loading branch information
0xcaff committed Dec 19, 2018
2 parents 8117bf7 + 7d62861 commit bfe283c
Show file tree
Hide file tree
Showing 13 changed files with 199 additions and 80 deletions.
64 changes: 64 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Contributing to forte-music/web

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.

## 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.

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`.

## 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/`.

* 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`).

* 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`).

[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.

## 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.

## Tests

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.

## 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`.

## 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.

[jest]: https://jestjs.io/
[prettier]: https://prettier.io/
[storybook]: https://storybook.js.org/
[tslint]: https://palantir.github.io/tslint/
[tasks]: ./.circleci/config.yml
[styled-components]: https://www.styled-components.com/
[render-props]: https://reactjs.org/docs/render-props.html
[container-component]: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
38 changes: 30 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,44 @@

[Check out the demo site!][demo-site]

Forte Music's web client. It's built using React and TypeScript.
## Quickstart

# Development
yarn query-codegen && yarn start-mock

git clone [email protected]:forte-music/web.git
yarn
yarn start-mock
## Build Configuration

This will run the web interface against an embedded mock graphql source.
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.

These features allow developing the frontend independently of the backend.

The `start-mock` and `storybook` scripts enable mock behavior for their corresponding actions.

## External Server

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.
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.

[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
[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
16 changes: 16 additions & 0 deletions src/components/AlbumGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

import {
Album as AlbumInfoAlbum,
AlbumInfo,
} from './AlbumsContainer/components/AlbumInfo';

interface Props {
albums: AlbumInfoAlbum[];
}

export const AlbumGrid = (props: Props) => (
<React.Fragment>
{props.albums.map(album => <AlbumInfo key={album.id} album={album} />)}
</React.Fragment>
);
31 changes: 12 additions & 19 deletions src/components/AlbumSearchResultsContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import Observer from 'react-intersection-observer';
import { AlbumsQuery, Result } from './enhancers/query';
import { AlbumSearchResultsLoadingContainer } from '../AlbumSearchResultsLoadingContainer';
import { AlbumInfo } from '../AlbumsContainer/components/AlbumInfo';
import { ArtworkGrid } from '../styled/search';
import { ArtworkGrid } from '../styled/artworkGrid';
import { OnEnterView } from '../OnEnterView';
import { AlbumGrid } from '../AlbumGrid';

import { calcArtworkPageSize } from '../../styled-mixins/artworkGrid';

interface Props {
// Whether or not to load more items when scrolled to the bottom of the page.
Expand All @@ -16,7 +18,10 @@ interface Props {
// Fetches data and renders album results of search results pages.
export const AlbumSearchResultsContainer = (props: Props) => (
<AlbumsQuery
variables={{ query: props.query, first: props.loadMore ? 30 : 6 }}
variables={{
query: props.query,
first: props.loadMore ? calcArtworkPageSize() : 6,
}}
>
{(result: Result) => (
<AlbumSearchResultsLoadingContainer
Expand All @@ -29,23 +34,13 @@ export const AlbumSearchResultsContainer = (props: Props) => (
children={albums => (
<React.Fragment>
<ArtworkGrid>
{albums.map(album => <AlbumInfo key={album.id} album={album} />)}
<AlbumGrid albums={albums} />

{props.loadMore &&
!result.loading &&
result.data &&
result.data.albums.pageInfo.hasNextPage && (
<Observer
key={'final'}
onChange={inView => {
if (!inView) {
return;
}

result.getNextPage();
}}
>
<div />
</Observer>
<OnEnterView onView={result.getNextPage} />
)}
</ArtworkGrid>
</React.Fragment>
Expand All @@ -54,5 +49,3 @@ export const AlbumSearchResultsContainer = (props: Props) => (
)}
</AlbumsQuery>
);

// TODO: Share Loading More Logic With AlbumsPage
31 changes: 10 additions & 21 deletions src/components/AlbumsContainer/components/AlbumsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import Observer from 'react-intersection-observer';

import Title from '../../Title';
import { AlbumInfo } from './AlbumInfo';
import { PositionedHeading } from '../../styled/PositionedHeading';
import { Container } from '../../Container';
import { ArtworkGridContents } from '../../styled/ArtworkGridContents';
import { ArtworkGrid } from '../../styled/artworkGrid';
import { Contents } from '../../styled/Contents';
import { OnEnterView } from '../../OnEnterView';
import { AlbumGrid } from '../../AlbumGrid';

import { AlbumsQuery_albums as Albums } from '../enhancers/__generated__/AlbumsQuery';

Expand All @@ -21,24 +22,12 @@ export const AlbumsPage = ({ albums, fetchMore }: Props) => (
<Container>
<AlbumsHeading>Albums</AlbumsHeading>

<ArtworkGridContents>
{albums &&
albums.edges.map(({ node }) => (
<AlbumInfo key={node.id} album={node} />
))}
<Observer
key={'final'}
onChange={inView => {
if (!inView) {
return;
}

fetchMore();
}}
>
<div />
</Observer>
</ArtworkGridContents>
<Contents>
<ArtworkGrid>
{albums && <AlbumGrid albums={albums.edges.map(edge => edge.node)} />}
<OnEnterView onView={fetchMore} />
</ArtworkGrid>
</Contents>
</Container>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/AlbumsContainer/enhancers/query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
} from './__generated__/AlbumsQuery';

const query = gql`
query AlbumsQuery($cursor: String) {
albums(first: 30, after: $cursor) @connection {
query AlbumsQuery($cursor: String, $pageSize: Int) {
albums(first: $pageSize, after: $cursor) @connection {
count
pageInfo {
hasNextPage
Expand Down
4 changes: 3 additions & 1 deletion src/components/AlbumsContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import React from 'react';
import { AlbumsQuery, Result } from './enhancers/query';
import { AlbumsPage } from './components/AlbumsPage';

import { calcArtworkPageSize } from '../../styled-mixins/artworkGrid';

export const Albums = () => (
<AlbumsQuery>
<AlbumsQuery variables={{ pageSize: calcArtworkPageSize() }}>
{(result: Result) => {
if (result.loading || !result.data) {
return null;
Expand Down
34 changes: 18 additions & 16 deletions src/components/ArtistPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Pluralize } from './Pluralize';
import { HeaderContainer } from './styled/HeaderContainer';
import { Heading } from './styled/Heading';
import { Contents } from './styled/Contents';
import { ArtworkGridContents } from './styled/ArtworkGridContents';
import { ArtworkGrid } from './styled/artworkGrid';

import { ArtistQuery_artist as Artist } from './ArtistContainer/enhancers/__generated__/ArtistQuery';

Expand All @@ -34,21 +34,23 @@ export const ArtistPage = (props: Props) => (
</HeaderContainer>

<Container>
<ArtworkGridContents>
{props.artist.albums.map(album => (
<ArtworkTwoInfo
key={album.id}
artwork={
<PlaybackAlbumArtwork
handlesBackgroundInteraction={false}
album={album}
/>
}
lineOne={<AlbumLink album={album} />}
lineTwo={album.releaseYear}
/>
))}
</ArtworkGridContents>
<Contents>
<ArtworkGrid>
{props.artist.albums.map(album => (
<ArtworkTwoInfo
key={album.id}
artwork={
<PlaybackAlbumArtwork
handlesBackgroundInteraction={false}
album={album}
/>
}
lineOne={<AlbumLink album={album} />}
lineTwo={album.releaseYear}
/>
))}
</ArtworkGrid>
</Contents>
</Container>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions src/components/KeyboardInteraction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class KeyboardInteractionInner extends Component<Props> {
return; // Do nothing if the event was already processed
}

if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
return;
}

switch (e.code) {
case 'ArrowLeft':
case 'KeyH':
Expand Down
23 changes: 23 additions & 0 deletions src/components/OnEnterView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import Observer from 'react-intersection-observer';

interface Props {
// Called when the component comes into view.
onView: () => void;
}

// Calls a method on props when enters view. Often used to implement
// infinite scrolling.
export const OnEnterView = (props: Props) => (
<Observer
onChange={inView => {
if (!inView) {
return;
}

props.onView();
}}
>
<div />
</Observer>
);
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { StyledComponentClass } from 'styled-components';
import { Theme } from '../../theme';
import { Contents } from './Contents';
import styled from '../../styled-components';
import { artworkGrid } from '../../styled-mixins/artworkGrid';

// @ts-ignore
export const ArtworkGridContents: StyledComponentClass<
{},
Theme
> = Contents.extend`
export const ArtworkGrid: StyledComponentClass<{}, Theme> = styled.div`
${artworkGrid};
`;
8 changes: 0 additions & 8 deletions src/components/styled/search.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import styled from '../../styled-components';
import { StyledComponentClass } from 'styled-components';
import { Theme } from '../../theme';

import { artworkGrid } from '../../styled-mixins/artworkGrid';
import { Container } from '../Container';
import { FocusedTextInput } from '../FocusedTextInput';

// @ts-ignore
export const ArtworkGrid: StyledComponentClass<{}, Theme> = styled.div`
${artworkGrid};
`;

const placeholderColor = '#757575';
const focusedTextColor = '#ffffff';

Expand Down
15 changes: 15 additions & 0 deletions src/styled-mixins/artworkGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,18 @@ export const artworkGrid = css`
minmax(${props => props.theme.gridArtworkSize}, 1fr)
);
`;

// Over estimates the number of 150px album artworks which would fit onto the
// screen. Used to decide how many items will be queried per-page.
export const calcArtworkPageSize = (): number => {
const maxWidth = 1100;
const innerWindowHeight = window.innerHeight;
const drawingArea = maxWidth * innerWindowHeight;

const artworkSize = 150;
const artworkArea = artworkSize * artworkSize;
const extraAreaMultiplier = 2;

const artworks = Math.floor(drawingArea / artworkArea * extraAreaMultiplier);
return artworks;
};

0 comments on commit bfe283c

Please sign in to comment.