Browse Source

Add 'atlas/' from commit 'b0d9e86372a8f70ca70f8fd616c52c857ac39a2a'

git-subtree-dir: atlas
git-subtree-mainline: f9daebe9a18df987118a0b521788d87ceb8ab4e9
git-subtree-split: b0d9e86372a8f70ca70f8fd616c52c857ac39a2a
Pedro Semeano 4 years ago
commit
cfb3da0073
100 changed files with 3909 additions and 0 deletions
  1. 91 0
      .gitignore
  2. 6 0
      .huskyrc
  3. 4 0
      .prettierrc
  4. 81 0
      README.md
  5. 9 0
      babel.config.js
  6. 3 0
      enzyme.config.js
  7. 18 0
      jest.config.js
  8. 8 0
      lerna.json
  9. 20 0
      package.json
  10. 13 0
      packages/app/.babelrc
  11. 32 0
      packages/app/.eslintrc.js
  12. 13 0
      packages/app/__tests__/App.test.js
  13. 16 0
      packages/app/global.d.ts
  14. 41 0
      packages/app/package.json
  15. 16 0
      packages/app/public/index.html
  16. 9 0
      packages/app/public/style.css
  17. 33 0
      packages/app/src/App.tsx
  18. 41 0
      packages/app/src/components/ChannelHeader/ChannelHeader.tsx
  19. 3 0
      packages/app/src/components/ChannelHeader/index.ts
  20. 14 0
      packages/app/src/components/ScrollToTop/ScrollToTop.tsx
  21. 3 0
      packages/app/src/components/ScrollToTop/index.ts
  22. 7 0
      packages/app/src/index.tsx
  23. 13 0
      packages/app/src/store/actions/DummyAction.ts
  24. 9 0
      packages/app/src/store/index.ts
  25. 21 0
      packages/app/src/store/reducers/DummyReducer.ts
  26. 7 0
      packages/app/src/store/reducers/index.ts
  27. 21 0
      packages/app/src/store/types/DummyTypes.ts
  28. 64 0
      packages/app/src/views/ChannelView/ChannelView.tsx
  29. 1 0
      packages/app/src/views/ChannelView/index.tsx
  30. 57 0
      packages/app/src/views/ExploreView/ExploreView.tsx
  31. 1 0
      packages/app/src/views/ExploreView/index.tsx
  32. 57 0
      packages/app/src/views/VideoView/VideoView.tsx
  33. 1 0
      packages/app/src/views/VideoView/index.tsx
  34. 155 0
      packages/app/staticData.ts
  35. 13 0
      packages/app/tsconfig.json
  36. 32 0
      packages/components/.eslintrc.js
  37. 3 0
      packages/components/.storybook/.babelrc
  38. 35 0
      packages/components/.storybook/main.js
  39. 6 0
      packages/components/.storybook/preview.js
  40. 3 0
      packages/components/README.md
  41. 13 0
      packages/components/__tests__/Avatar.test.js
  42. 13 0
      packages/components/__tests__/Banner.test.js
  43. 15 0
      packages/components/__tests__/Button.test.js
  44. 20 0
      packages/components/__tests__/ChannelSummary.test.js
  45. 20 0
      packages/components/__tests__/DetailsTable.test.js
  46. 19 0
      packages/components/__tests__/Grid.test.js
  47. 13 0
      packages/components/__tests__/SearchBar.test.js
  48. 25 0
      packages/components/__tests__/Tag.test.js
  49. 17 0
      packages/components/__tests__/VideoPlayer.test.js
  50. 17 0
      packages/components/__tests__/VideoPreview.test.js
  51. 28 0
      packages/components/__tests__/__snapshots__/Avatar.test.js.snap
  52. 25 0
      packages/components/__tests__/__snapshots__/Banner.test.js.snap
  53. 70 0
      packages/components/__tests__/__snapshots__/Button.test.js.snap
  54. 377 0
      packages/components/__tests__/__snapshots__/ChannelSummary.test.js.snap
  55. 334 0
      packages/components/__tests__/__snapshots__/DetailsTable.test.js.snap
  56. 300 0
      packages/components/__tests__/__snapshots__/Grid.test.js.snap
  57. 84 0
      packages/components/__tests__/__snapshots__/SearchBar.test.js.snap
  58. 130 0
      packages/components/__tests__/__snapshots__/Tag.test.js.snap
  59. 148 0
      packages/components/__tests__/__snapshots__/VideoPlayer.test.js.snap
  60. 103 0
      packages/components/__tests__/__snapshots__/VideoPreview.test.js.snap
  61. 8 0
      packages/components/babel.config.json
  62. 11 0
      packages/components/now.json
  63. 67 0
      packages/components/package.json
  64. 44 0
      packages/components/rollup.config.js
  65. 17 0
      packages/components/scripts/build-index.js
  66. 28 0
      packages/components/src/components/Avatar/Avatar.style.tsx
  67. 11 0
      packages/components/src/components/Avatar/Avatar.tsx
  68. 3 0
      packages/components/src/components/Avatar/index.tsx
  69. 157 0
      packages/components/src/components/Button/Button.style.ts
  70. 29 0
      packages/components/src/components/Button/Button.tsx
  71. 3 0
      packages/components/src/components/Button/index.ts
  72. 23 0
      packages/components/src/components/Grid/Grid.style.ts
  73. 20 0
      packages/components/src/components/Grid/Grid.tsx
  74. 3 0
      packages/components/src/components/Grid/index.ts
  75. 30 0
      packages/components/src/components/Header/Header.style.ts
  76. 32 0
      packages/components/src/components/Header/Header.tsx
  77. 3 0
      packages/components/src/components/Header/index.ts
  78. 43 0
      packages/components/src/components/NavButton/NavButton.style.ts
  79. 22 0
      packages/components/src/components/NavButton/NavButton.tsx
  80. 3 0
      packages/components/src/components/NavButton/index.ts
  81. 24 0
      packages/components/src/components/Tag/Tag.style.ts
  82. 18 0
      packages/components/src/components/Tag/Tag.tsx
  83. 3 0
      packages/components/src/components/Tag/index.ts
  84. 38 0
      packages/components/src/components/TagButton/TagButton.style.ts
  85. 20 0
      packages/components/src/components/TagButton/TagButton.tsx
  86. 3 0
      packages/components/src/components/TagButton/index.ts
  87. 47 0
      packages/components/src/components/TextField/TextField.style.ts
  88. 84 0
      packages/components/src/components/TextField/TextField.tsx
  89. 3 0
      packages/components/src/components/TextField/index.ts
  90. 113 0
      packages/components/src/components/Typography/Typography.style.ts
  91. 20 0
      packages/components/src/components/Typography/Typography.tsx
  92. 3 0
      packages/components/src/components/Typography/index.ts
  93. 33 0
      packages/components/src/components/VideoPlayer/VideoPlayer.style.tsx
  94. 79 0
      packages/components/src/components/VideoPlayer/VideoPlayer.tsx
  95. 3 0
      packages/components/src/components/VideoPlayer/index.tsx
  96. 47 0
      packages/components/src/components/VideoPreview/VideoPreview.styles.tsx
  97. 47 0
      packages/components/src/components/VideoPreview/VideoPreview.tsx
  98. 3 0
      packages/components/src/components/VideoPreview/index.tsx
  99. 8 0
      packages/components/src/index.ts
  100. 5 0
      packages/components/src/theme/breakpoints.ts

+ 91 - 0
.gitignore

@@ -0,0 +1,91 @@
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Node Modules
+
+node_modules/
+
+
+
+# Secrets
+*.env*
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# cache and distribution folders
+.cache/
+.now
+dist/
+
+# Mac files
+.DS_Store
+
+# Yarn
+yarn-error.log
+.pnp/
+.pnp.js
+# Yarn Integrity file
+.yarn-integrity

+ 6 - 0
.huskyrc

@@ -0,0 +1,6 @@
+{
+    "hooks": {
+        "pre-commit": "lint-staged",
+        "pre-push": "yarn test"
+    }
+}

+ 4 - 0
.prettierrc

@@ -0,0 +1,4 @@
+{
+	"semi": false,
+	"trailingComma": "none"
+}

+ 81 - 0
README.md

@@ -0,0 +1,81 @@
+# atlas
+
+## About
+
+The components package holds the React components used in Atlas and their stories, while the app package contains the Atlas App itself.
+Given how the code is organized, the first time you clone the repo, you need to build the components package.
+
+## Getting Started
+
+After cloning the repo run:
+
+```bash
+$ cd atlas
+$ yarn install
+```
+
+### Components Package
+
+The components package is located under `./packages/components`, so every command that follows should be prefixed by
+
+```bash
+$ cd packages/components
+```
+
+To start storybook run
+
+```bash
+$ yarn storybook
+```
+
+To build the components package and use it elsewhere, for example inside the app package, you can run:
+
+```bash
+$ yarn build
+```
+
+When developing, you can run the bundler in watch mode with
+
+```bash
+$ yarn start
+```
+
+#### Populate `index.ts`
+
+Running
+
+```bash
+$ yarn index
+```
+
+will write import and exports from every file inside `src/components` to `src/index.ts`
+
+For the script to run properly make sure to follow the convention of naming a Component the same as the file is exported from.
+
+For example, exporting `Button` from `Button.tsx` will work while exporting `Cactus` from `Plant.tsx` will not.
+
+If you do not wish to follow this convention, you can just ignore the index script and run
+
+```bash
+$ yarn build
+```
+
+#### App package
+
+The components package is located under `./packages/app`, so every command that follows should be prefixed by
+
+```bash
+$ cd packages/app
+```
+
+Then run
+
+```bash
+$ yarn dev
+```
+
+To start the app on `localhost:1234`
+
+## Deploy Storybook as a static site
+
+To deploy storybook as a static site, a `now.json` file has been setup for deployment with [Zeit Now](https://now.sh).

+ 9 - 0
babel.config.js

@@ -0,0 +1,9 @@
+module.exports = {
+  presets: [
+    "@babel/preset-env",
+    "@babel/preset-react"
+  ],
+  plugins: [
+    "transform-export-extensions"
+  ]
+}

+ 3 - 0
enzyme.config.js

@@ -0,0 +1,3 @@
+import Enzyme from 'enzyme'
+import Adapter from 'enzyme-adapter-react-16'
+Enzyme.configure({ adapter: new Adapter() })

+ 18 - 0
jest.config.js

@@ -0,0 +1,18 @@
+module.exports = {
+  preset: "ts-jest",
+  setupFiles: [
+    "<rootDir>/enzyme.config.js"
+  ],
+  moduleFileExtensions: [
+    "js",
+    "ts",
+    "tsx"
+  ],
+  transform: {
+    "^.+\\.tsx?$": "ts-jest",
+    "^.+\\.jsx?$": "babel-jest"
+  },
+  snapshotSerializers: [
+    "enzyme-to-json/serializer"
+  ]
+}

+ 8 - 0
lerna.json

@@ -0,0 +1,8 @@
+{
+  "packages": [
+    "packages/*"
+  ],
+  "version": "independent",
+  "npmClient": "yarn",
+  "useWorkspaces": true
+}

+ 20 - 0
package.json

@@ -0,0 +1,20 @@
+{
+  "name": "atlas",
+  "private": true,
+  "workspaces": [
+    "packages/*"
+  ],
+  "scripts": {
+    "test": "jest",
+    "tdd": "jest --watch"
+  },
+  "lint-staged": {
+    "*.{ts, tsx, js, jsx, json}": [
+      "prettier --no-semi --trailing-comma none --write"
+    ]
+  },
+  "devDependencies": {
+    "husky": "^4.2.5",
+    "lerna": "^3.20.2"
+  }
+}

+ 13 - 0
packages/app/.babelrc

@@ -0,0 +1,13 @@
+{
+  "presets": [
+    "react",
+    [
+      "env",
+      {
+        "targets": {
+          "browsers": ["last 2 versions"]
+        }
+      }
+    ]
+  ]
+}

+ 32 - 0
packages/app/.eslintrc.js

@@ -0,0 +1,32 @@
+module.exports = {
+  env: {
+    browser: true,
+    es6: true,
+  },
+  globals: {
+    Atomics: "readonly",
+    SharedArrayBuffer: "readonly",
+  },
+  parser: "@typescript-eslint/parser",
+  parserOptions: {
+    ecmaFeatures: {
+      jsx: true,
+    },
+    ecmaVersion: 2019,
+    sourceType: "module",
+  },
+  extends: [
+    "plugin:react/recommended",
+    "standard",
+    "plugin:jsx-a11y/recommended",
+    "plugin:prettier/recommended",
+  ],
+  plugins: ["react", "react-hooks"],
+  rules: {
+    "react-hooks/rules-of-hooks": "error",
+    "react-hooks/exhaustive-deps": "warn",
+  },
+  settings: {
+    version: "detect",
+  },
+};

+ 13 - 0
packages/app/__tests__/App.test.js

@@ -0,0 +1,13 @@
+import React from "react"
+import { shallow } from "enzyme"
+import App from "./../src/App"
+
+describe("App component", () => {
+
+  const component = shallow(<App />)
+
+  it("Should render.", () => {
+    expect(component).toBeDefined()
+  })
+
+})

+ 16 - 0
packages/app/global.d.ts

@@ -0,0 +1,16 @@
+/* 
+
+I was getting this error while running Jest:
+
+packages/app/src/views/ExploreView/ExploreView.tsx:15:48 - error 
+TS2339: Property 'flat' does not exist on type 'unknown[]'.
+
+I tried adding "ES2019" and "ES2019.Array" to Typescript lib compiler options to no avail.
+The only way I was able to fix this issue was to add 'flat' to Typescript globals (this file).
+
+ */
+
+interface Array<T> {
+  flat(): Array<T>
+  flatMap(func: (x: T) => T): Array<T>
+}

+ 41 - 0
packages/app/package.json

@@ -0,0 +1,41 @@
+{
+  "name": "app",
+  "version": "1.0.0",
+  "description": "A user governed video platform",
+  "homepage": "https://github.com/Joystream/atlas#readme",
+  "license": "ISC",
+  "main": "src/app.js",
+  "directories": {
+    "src": "src",
+    "test": "__tests__"
+  },
+  "files": [
+    "src"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/Joystream/atlas.git"
+  },
+  "scripts": {
+    "start": "parcel public/index.html",
+    "dev": "parcel public/index.html",
+    "test": "echo \"Error: run tests from root\" && exit 1"
+  },
+  "bugs": {
+    "url": "https://github.com/Joystream/atlas/issues"
+  },
+  "dependencies": {
+    "@reach/router": "^1.3.3",
+    "normalize.css": "^8.0.1",
+    "packages": "^0.0.8",
+    "react": "^16.13.0",
+    "react-dom": "^16.13.0",
+    "react-redux": "^7.2.0",
+    "redux": "^4.0.5"
+  },
+  "devDependencies": {
+    "@types/reach__router": "^1.3.1",
+    "@types/react-redux": "^7.1.7",
+    "babel-core": "^6.26.3"
+  }
+}

+ 16 - 0
packages/app/public/index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="stylesheet" href="./style.css">
+    <title>Atlas</title>
+</head>
+
+<body>
+    <div id="root">not rendered</div>
+    <script src="../src/index.tsx"></script>
+</body>
+
+</html>

+ 9 - 0
packages/app/public/style.css

@@ -0,0 +1,9 @@
+@charset "UTF-8";
+
+html {
+  font-family: "Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif";
+}
+
+body {
+  font-size: 100%;
+}

+ 33 - 0
packages/app/src/App.tsx

@@ -0,0 +1,33 @@
+import React from "react"
+import { Router } from "@reach/router"
+import { Provider } from "react-redux"
+
+import store from "./store"
+import data from "../staticData"
+
+import ChannelView from "./views/ChannelView"
+import ExploreView from "./views/ExploreView"
+import VideoView from "./views/VideoView"
+import ScrollToTop from "./components/ScrollToTop"
+
+let { channels, videos } = data
+
+export default function App() {
+  return (
+    <main className="main-section">
+      <Provider store={store}>
+        <Router primary={false}>
+          <ScrollToTop path="/">
+            <ExploreView path="/" channels={channels} videos={videos} />
+            <ChannelView
+              path="/channels/:channelName"
+              channels={channels}
+              videos={videos}
+            />
+            <VideoView path="/videos/:idx" channels={channels} videos={videos} />
+          </ScrollToTop>
+        </Router>
+      </Provider>
+    </main>
+  )
+}

+ 41 - 0
packages/app/src/components/ChannelHeader/ChannelHeader.tsx

@@ -0,0 +1,41 @@
+import React from "react"
+import { navigate } from "@reach/router"
+
+import { Banner, ChannelSummary } from "components"
+
+type ChannelHeaderProps = {
+  img?: string
+  name: string
+  banner?: string
+  isPublic?: boolean
+  isVerified?: boolean
+  description?: string
+  channelUrl?: string
+}
+
+function ChannelHeader({
+  img,
+  isPublic = true,
+  isVerified = false,
+  description,
+  name,
+  banner,
+  channelUrl,
+}: ChannelHeaderProps) {
+  return (
+    <>
+      {banner && <Banner src={banner} />}
+      <ChannelSummary
+        name={name}
+        isPublic={isPublic}
+        isVerified={isVerified}
+        size="large"
+        img={img}
+        description={description}
+        onClick={() => navigate(channelUrl)}
+      />
+    </>
+  )
+}
+
+export default ChannelHeader

+ 3 - 0
packages/app/src/components/ChannelHeader/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import ChannelHeader from "./ChannelHeader"
+export default memo(ChannelHeader)

+ 14 - 0
packages/app/src/components/ScrollToTop/ScrollToTop.tsx

@@ -0,0 +1,14 @@
+import React, { useEffect } from "react"
+
+type ScrollToTopProps = {
+  path: string
+  children?: any
+  location?: any
+}
+
+const ScrollToTop = ({ children, location }: ScrollToTopProps) => {
+  useEffect(() => window.scrollTo(0, 0), [location.pathname])
+  return children
+}
+
+export default ScrollToTop

+ 3 - 0
packages/app/src/components/ScrollToTop/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import ScrollToTop from "./ScrollToTop"
+export default memo(ScrollToTop)

+ 7 - 0
packages/app/src/index.tsx

@@ -0,0 +1,7 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
+import "normalize.css";
+import "../public/style.css";
+
+ReactDOM.render(<App />, document.getElementById("root"));

+ 13 - 0
packages/app/src/store/actions/DummyAction.ts

@@ -0,0 +1,13 @@
+// This is just a placeholder. It should be used as a guideline and then deleted.
+
+import { ADD_DUMMY_ACTION, REMOVE_DUMMY_ACTION, Dummy, DummyActionTypes } from "./../types/DummyTypes"
+
+export const AddDummy = (dummy: Dummy): DummyActionTypes => ({
+  type: ADD_DUMMY_ACTION,
+  dummy
+})
+
+export const RemoveDummy = (id: string): DummyActionTypes => ({
+  type: REMOVE_DUMMY_ACTION,
+  id
+})

+ 9 - 0
packages/app/src/store/index.ts

@@ -0,0 +1,9 @@
+import { createStore } from "redux"
+import rootReducer from "./reducers"
+
+const store = createStore(
+  rootReducer,
+  (<any>window).__REDUX_DEVTOOLS_EXTENSION__ && (<any>window).__REDUX_DEVTOOLS_EXTENSION__()
+)
+
+export default store

+ 21 - 0
packages/app/src/store/reducers/DummyReducer.ts

@@ -0,0 +1,21 @@
+// This is just a placeholder. It should be used as a guideline and then deleted.
+
+import { Dummy, DummyActionTypes, ADD_DUMMY_ACTION, REMOVE_DUMMY_ACTION } from "./../types/DummyTypes"
+
+const initialState: Dummy[] = []
+
+const DummyReducer = (state = initialState, action: DummyActionTypes): Dummy[] => {
+  switch (action.type) {
+    case ADD_DUMMY_ACTION:
+      return [
+        ...state,
+        action.dummy
+      ]
+    case REMOVE_DUMMY_ACTION:
+      return state.filter(dummy => dummy.id !== action.id)
+    default:
+      return state
+  }
+}
+
+export default DummyReducer

+ 7 - 0
packages/app/src/store/reducers/index.ts

@@ -0,0 +1,7 @@
+import { combineReducers } from "redux"
+import DummyReducer from "./DummyReducer"
+
+
+export default combineReducers({
+  DummyReducer
+})

+ 21 - 0
packages/app/src/store/types/DummyTypes.ts

@@ -0,0 +1,21 @@
+// This is just a placeholder. It should be used as a guideline and then deleted.
+
+export const ADD_DUMMY_ACTION = "ADD_DUMMY_ACTION"
+export const REMOVE_DUMMY_ACTION = "REMOVE_DUMMY_ACTION"
+
+export interface Dummy {
+  id: string
+  name: string
+}
+
+interface AddDummyAction {
+  type: typeof ADD_DUMMY_ACTION
+  dummy: Dummy
+}
+
+interface RemoveDummyAction {
+  type: typeof REMOVE_DUMMY_ACTION
+  id: string
+}
+
+export type DummyActionTypes = AddDummyAction | RemoveDummyAction

+ 64 - 0
packages/app/src/views/ChannelView/ChannelView.tsx

@@ -0,0 +1,64 @@
+import React from "react"
+import { RouteComponentProps, useParams, navigate } from "@reach/router"
+import { GenericSection, VideoPreview, Grid } from "components"
+import ChannelHeader from "./../../components/ChannelHeader"
+
+type ChannelProps = {
+  name: string
+  isPublic?: boolean
+  isVerified?: boolean
+  description?: string
+  banner?: string
+  videos?: any[]
+  img: string
+}
+
+function ChannelComponent({
+  name,
+  isPublic = true,
+  isVerified = false,
+  description,
+  banner,
+  videos,
+  img,
+}: ChannelProps) {
+  return (
+    <>
+      <ChannelHeader
+        name={name}
+        isPublic={isPublic}
+        isVerified={isVerified}
+        description={description}
+        banner={banner}
+        img={img}
+      />
+      <GenericSection title="Videos">
+        <Grid
+          minItemWidth="250"
+          maxItemWidth="600"
+          items={videos.map((video, idx) => (
+            <VideoPreview
+              onClick={() => navigate(`/videos/${idx}`)}
+              onChannelClick={() => navigate(`/channels/${video.channel}`)}
+              key={`title-${idx}`}
+              title={video.title}
+              poster={video.poster}
+            />
+          ))}
+        />
+      </GenericSection>
+    </>
+  )
+}
+
+type RouteProps = {
+  videos: any
+  channels: any
+} & RouteComponentProps
+
+export default function Channel({ videos, channels }: RouteProps) {
+  let params = useParams()
+  let channelVideos = videos[params.channelName]
+  let channel = channels[params.channelName]
+  return <ChannelComponent {...channel} videos={channelVideos} />
+}

+ 1 - 0
packages/app/src/views/ChannelView/index.tsx

@@ -0,0 +1 @@
+export { default } from "./ChannelView";

+ 57 - 0
packages/app/src/views/ExploreView/ExploreView.tsx

@@ -0,0 +1,57 @@
+import React from "react"
+import { RouteComponentProps, navigate } from "@reach/router"
+import { GenericSection, VideoPreview, ChannelSummary, Grid } from "components"
+
+type ExploreViewProps = {
+  videos: any
+  channels: any
+} & RouteComponentProps
+
+export default function ExploreView({
+  channels,
+  videos
+}: ExploreViewProps) {
+
+  let allVideos: any[] = Object.values(videos).flat()
+  let allChannels: any[] = Object.values(channels)
+
+  return (
+    <>
+      <GenericSection topDivider title="Latest Videos" linkText="All Videos" onLinkClick={() => {}}>
+        <Grid
+          minItemWidth="250"
+          items={allVideos.map((video, idx) => {
+            let { img: channelImg } = channels[video.channel] || ""
+            return (
+              <VideoPreview
+                key={`${video.title}-${idx}`}
+                channelImg={channelImg}
+                channel={video.channel}
+                title={video.title}
+                poster={video.poster}
+                showChannel
+                onClick={() => navigate(`videos/${idx}`)}
+                onChannelClick={() => navigate(`channels/${video.channel}`)}
+              />
+            )
+          })}
+        />
+      </GenericSection>
+      <GenericSection topDivider title="Latest video channels" linkText="All Channels" onLinkClick={() => {}}>
+        <div className="channel-gallery">
+          {allChannels.map((channel, idx) => (
+            <ChannelSummary
+              key={`${channel.name}-${idx}`}
+              img={channel.img}
+              size="default"
+              name={channel.name}
+              isPublic={channel.isPublic}
+              isVerified={channel.isVerified}
+              onClick={() => navigate(`channels/${channel.name}`)}
+            />
+          ))}
+        </div>
+      </GenericSection>
+    </>
+  )
+}

+ 1 - 0
packages/app/src/views/ExploreView/index.tsx

@@ -0,0 +1 @@
+export { default } from "./ExploreView";

+ 57 - 0
packages/app/src/views/VideoView/VideoView.tsx

@@ -0,0 +1,57 @@
+import React from "react"
+
+import {
+  VideoPlayer,
+  GenericSection,
+  ChannelSummary,
+  DetailsTable,
+} from "components"
+import { useParams, RouteComponentProps, navigate } from "@reach/router"
+
+type VideoViewProps = {
+  video: any
+  channel: any
+}
+function VideoViewComponent({ video, channel }: VideoViewProps) {
+  return (
+    <>
+      <GenericSection>
+        <VideoPlayer src={video.src} poster={video.poster} />
+      </GenericSection>
+      <GenericSection
+        topDivider
+        title={video.title}
+        className="video-details"
+      >
+        <ChannelSummary
+          isPublic={channel.isPublic}
+          img={channel.img}
+          name={channel.name}
+          isVerified={channel.isVerified}
+          description={video.description}
+          size="default"
+          onClick={() => navigate(`/channels/${channel.name}`)}
+        />
+      </GenericSection>
+      <GenericSection
+        topDivider
+        title="Video details"
+        className="video-details-table"
+      >
+        <DetailsTable details={video.details} />
+      </GenericSection>
+    </>
+  )
+}
+
+type RouteProps = {
+  videos: any
+  channels: any
+} & RouteComponentProps
+
+export default function VideoView({ videos, channels }: RouteProps) {
+  let params = useParams()
+  let video: any = Object.values(videos).flat()[params.idx]
+  let channel = channels[video.channel]
+  return <VideoViewComponent video={video} channel={channel} />
+}

+ 1 - 0
packages/app/src/views/VideoView/index.tsx

@@ -0,0 +1 @@
+export { default } from "./VideoView";

+ 155 - 0
packages/app/staticData.ts

@@ -0,0 +1,155 @@
+export default {
+  channels: {
+    "Kek-Mex's video channel": {
+      name: "Kek-Mex's video channel",
+      isPublic: true,
+      isVerified: true,
+      description: "I uploade public domain films every now and then :)",
+      img:
+        "https://s3.amazonaws.com/keybase_processed_uploads/9003a57620356bd89d62bd34c7c0c305_360_360.jpg",
+    },
+    "How to Draw": {
+      name: "How to Draw",
+      description: "Learn the techniques I use to make my drawings",
+      isPublic: true,
+    },
+    "Staked Podcast": {
+      name: "Staked Podcast",
+      isPublic: true,
+      isVerified: true,
+      img:
+        "https://ssl-static.libsyn.com/p/assets/c/1/f/f/c1fff8ce376f9eb9/height_90_width_90_iTunes_Cover.png",
+      banner:
+        "http://static.libsyn.com/p/assets/2/c/2/5/2c25acab892a768e/Twitter_Cover.png",
+    },
+  },
+  videos: {
+    "Kek-Mex's video channel": [
+      {
+        title: "Reefer Madness (1936)",
+        channel: "Kek-Mex's video channel",
+        description:
+          'A trio of drug dealers lead innocent teenagers to become addicted to "reefer" cigarettes by holding wild parties with jazz music.',
+        poster:
+          "https://upload.wikimedia.org/wikipedia/commons/3/37/Reefer_Madness_%281936%29.jpg",
+        src:
+          "https://joystreamnode1.cloudstorey.in/asset/v0/5Ee2hm9KR2r1r5gjxYxZZG4wwyC4UN1VkbQsK9pbNKE2DphZ",
+        details: {
+          explicit: "no",
+          "first released": "1936-01-01",
+          language: "english",
+          category: "comedy",
+          license: "public domain",
+          attribution: "George A. Hirliman Productions",
+        },
+      },
+      {
+        title: "The Charlie Chaplin Festival (1941)",
+        channel: "Kek-Mex's video channel",
+        description:
+          "Four Chaplin shorts from 1917: The Adventurer, The Cure, Easy Street and The Immigrant, presented with music and sound effects.",
+        src:
+          "https://joystream.proxy.web.id/asset/v0/5HKEjuDTh5gS2MY2j58UVvrLSwdapxxHMJ9TWuPqCVS6DvKr",
+        poster:
+          "https://image.tmdb.org/t/p/w600_and_h900_bestv2/kZqVmoHksjX1FANINggnaoCmwIn.jpg",
+        details: {
+          explicit: "No",
+          "first released": "1941-04-01",
+          language: "english",
+          category: "comedy",
+          license: "public Domain",
+          Attribution: "Charles Chaplin",
+        },
+      },
+      {
+        title: "Frankenstein (1910)",
+        channel: "Kek-Mex's video channel",
+        description:
+          "This 14-minute short film, was the first motion picture adaptation of Mary Shelley's Frankenstein.",
+        poster:
+          "https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Frankenstein_%281910%29_poster.jpg/800px-Frankenstein_%281910%29_poster.jpg",
+        src:
+          "https://www.computt.com/asset/v0/5GdAwDFSDQaWobZV2iiwTyGeX1zLKWri7ddC2WY9Ri2sTgoB",
+        details: {
+          explicit: "no",
+          "first released": "1910-03-17",
+          language: "english",
+          category: "Film & Animation",
+          license: "Public Domain",
+          attribution: "Edison Studios",
+        },
+      },
+      {
+        title: "Doomsday Machine (1972)",
+        channel: "Kek-Mex's video channel",
+        poster:
+          "https://m.media-amazon.com/images/M/MV5BMDRiNzUxNGUtNzVmYS00NmJkLWIxYzYtMGUwMzJlZmU1MWZkXkEyXkFqcGdeQXVyMzUzNTU3Mw@@._V1_.jpg",
+        src:
+          "https://rome-node-6.joystream.org/asset/v0/5EVT9ujAzG3yYzd6GsJhZ1tAJZPCVnveYLhk3sPYJesafcpC",
+        description:
+          "The Chinese have developed a doomsday device and the U.S. fears the will use it. A manned mission to Venus is stepped up. At the last minute three of the male crew are replaced with three female crew members. Once they are on there way to Venus the reason becomes apparent.",
+        details: {
+          explicit: "no",
+          "first released": "1971-12-31",
+          language: "english",
+          category: "Film & Animation",
+          license: "public Domain",
+        },
+      },
+      {
+        title: "House on Haunted Hill (1959)",
+        channel: "Kek-Mex's video channel",
+        description:
+          "Frederick Loren has invited five strangers to a party of a lifetime. He is offering each of them $10,000 if they can stay the night in a house.",
+        poster:
+          "https://upload.wikimedia.org/wikipedia/commons/thumb/2/24/House_on_Haunted_Hill.jpg/800px-House_on_Haunted_Hill.jpg",
+        src:
+          "https://rome-node-6.joystream.org/asset/v0/5GJdg38g5PNC1QhRV6ETJf8BhpFP8jChdrHoXc6Dyk68EUc5",
+        details: {
+          explicit: "no",
+          "first released": "1959-02-16",
+          language: "english",
+          category: "film & animation",
+          license: "public domain",
+          attribution: "William castle Production",
+        },
+      },
+    ],
+    "How to Draw": [
+      {
+        title: "How to Draw bitcoin coming out of the ground",
+        channel: "How To Draw",
+        description:
+          "Learn how to draw a hand coming out of the ground holding a bitcoin",
+        poster: "test",
+        src:
+          "https://joystream.proxy.web.id/asset/v0/5GbZj1ENkRNVqs7kZ4mZwQNZc4ujpm3qGnEw4QoDepAcYXeY",
+        details: {
+          explicit: "no",
+          "first released": "	2020-02-29",
+          language: "english",
+          license: "original content",
+        },
+      },
+    ],
+    "Staked Podcast": [
+      {
+        title: "Staked ep1 - Introduced",
+        description: "Still WIP - 2",
+        channel: "Staked Podcast",
+        poster:
+          "https://ssl-static.libsyn.com/p/assets/a/4/8/f/a48f1a0697e958ce/Cover_2.png",
+        src:
+          "https://www.computt.com/asset/v0/5EjgEKNpyDbNdjcJoZ8izWuzeDUtAcaUvwG8vUWZQZ256NLb",
+        details: {
+          explicit: "yes",
+          "first released": "2019-02-27",
+          language: "English",
+          category: "Science & Technology",
+          license: "Original content",
+          attribution: "",
+        },
+      },
+    ],
+  },
+};

+ 13 - 0
packages/app/tsconfig.json

@@ -0,0 +1,13 @@
+{
+  "extends": "../../tsconfig.json",
+
+  "compilerOptions": {
+    "jsx": "react",
+    "declaration": true,
+    "declarationDir": "dist",
+    "lib": [
+      "ES2019",
+      "ES2019.Array"
+    ]
+  }
+}

+ 32 - 0
packages/components/.eslintrc.js

@@ -0,0 +1,32 @@
+module.exports = {
+  env: {
+    browser: true,
+    es6: true,
+  },
+  globals: {
+    Atomics: "readonly",
+    SharedArrayBuffer: "readonly",
+  },
+  parser: "@typescript-eslint/parser",
+  parserOptions: {
+    ecmaFeatures: {
+      jsx: true,
+    },
+    ecmaVersion: 2019,
+    sourceType: "module",
+  },
+  extends: [
+    "plugin:react/recommended",
+    "standard",
+    "plugin:jsx-a11y/recommended",
+    "plugin:prettier/recommended",
+  ],
+  plugins: ["react", "react-hooks"],
+  rules: {
+    "react-hooks/rules-of-hooks": "error",
+    "react-hooks/exhaustive-deps": "warn",
+  },
+  settings: {
+    version: "detect",
+  },
+};

+ 3 - 0
packages/components/.storybook/.babelrc

@@ -0,0 +1,3 @@
+{
+  "presets": ["@emotion/babel-preset-css-prop"]
+}

+ 35 - 0
packages/components/.storybook/main.js

@@ -0,0 +1,35 @@
+module.exports = {
+  stories: ["../stories/**/*.stories.(js|jsx|ts|tsx|mdx)"],
+  addons: [
+    "@storybook/addon-actions",
+    "@storybook/addon-links",
+    "@storybook/addon-knobs",
+    "@storybook/addon-storysource",
+    "storybook-addon-jsx/register",
+    "@storybook/addon-docs",
+  ],
+  webpackFinal: async config => {
+    config.module.rules.push({
+      test: /\.(ts|tsx)$/,
+      use: [
+        {
+          loader: require.resolve("babel-loader"),
+          options: {
+            presets: [
+              "@babel/preset-env",
+              "@babel/preset-typescript",
+              "@babel/preset-react",
+              "@emotion/babel-preset-css-prop",
+            ],
+          },
+        },
+        // Optional
+        {
+          loader: require.resolve("react-docgen-typescript-loader"),
+        },
+      ],
+    });
+    config.resolve.extensions.push(".ts", ".tsx");
+    return config;
+  },
+};

+ 6 - 0
packages/components/.storybook/preview.js

@@ -0,0 +1,6 @@
+import { addDecorator } from "@storybook/react";
+import { withKnobs } from "@storybook/addon-knobs";
+import { jsxDecorator } from "storybook-addon-jsx";
+
+addDecorator(withKnobs);
+addDecorator(jsxDecorator);

+ 3 - 0
packages/components/README.md

@@ -0,0 +1,3 @@
+# atlas-components
+
+A components library for the Atlas Side Project, the storybook for the components is available [here](https://atlas-components.now.sh)

+ 13 - 0
packages/components/__tests__/Avatar.test.js

@@ -0,0 +1,13 @@
+import React from "react"
+import { mount } from "enzyme"
+import Avatar from "./../src/components/Avatar"
+
+describe("Avatar component", () => {
+
+  const component = mount(<Avatar img="https://s3.amazonaws.com/keybase_processed_uploads/9003a57620356bd89d62bd34c7c0c305_360_360.jpg" />)
+
+  it("Should render correctly", () => {
+    expect(component).toMatchSnapshot()
+  })
+
+})

+ 13 - 0
packages/components/__tests__/Banner.test.js

@@ -0,0 +1,13 @@
+import React from "react"
+import { mount } from "enzyme"
+import Banner from "./../src/components/Banner"
+
+describe("Banner component", () => {
+
+  const component = mount(<Banner src="http://static.libsyn.com/p/assets/2/c/2/5/2c25acab892a768e/Twitter_Cover.png" />)
+
+  it("Should render correctly", () => {
+    expect(component).toMatchSnapshot()
+  })
+
+})

+ 15 - 0
packages/components/__tests__/Button.test.js

@@ -0,0 +1,15 @@
+import React from "react"
+import { mount } from "enzyme"
+import Button from "./../src/components/Button"
+
+describe("Button component", () => {
+
+  it("Should render default button correctly", () => {
+    expect(mount(<Button>Click me!</Button>)).toMatchSnapshot()
+  })
+
+  it("Should render custom button correctly", () => {
+    expect(mount(<Button size="large" color="success">Hello Atlas</Button>)).toMatchSnapshot()
+  })
+
+})

+ 20 - 0
packages/components/__tests__/ChannelSummary.test.js

@@ -0,0 +1,20 @@
+import React from "react"
+import { mount } from "enzyme"
+import ChannelSummary from "./../src/components/ChannelSummary"
+
+describe("ChannelSummary component", () => {
+
+  const component = mount(
+    <ChannelSummary
+      img={"https://s3.amazonaws.com/keybase_processed_uploads/9003a57620356bd89d62bd34c7c0c305_360_360.jpg"}
+      size="default"
+      name={"Test channel"}
+      isPublic={true}
+      isVerified={true}
+    />)
+
+  it("Should render correctly", () => {
+    expect(component).toMatchSnapshot()
+  })
+
+})

+ 20 - 0
packages/components/__tests__/DetailsTable.test.js

@@ -0,0 +1,20 @@
+import React from "react"
+import { mount } from "enzyme"
+import DetailsTable from "./../src/components/DetailsTable"
+
+describe("DetailsTable component", () => {
+
+  const component = mount(<DetailsTable details={{
+    explicit: "yes",
+    "first released": "2019-02-27",
+    language: "English",
+    category: "Science & Technology",
+    license: "Original content",
+    attribution: ""
+  }} />)
+
+  it("Should render correctly", () => {
+    expect(component).toMatchSnapshot()
+  })
+
+})

+ 19 - 0
packages/components/__tests__/Grid.test.js

@@ -0,0 +1,19 @@
+import React from "react"
+import { mount } from "enzyme"
+import Grid from "./../src/components/Grid"
+
+describe("Grid component", () => {
+
+  const Item = ({ text }) => <div>{text}</div>
+
+  const component = mount(<Grid
+      minItemWidth="250"
+      maxItemWidth="600"
+      items={[...Array(10).keys()].map((i, index) => <Item key={index} text={`test-${i}`} />)}
+    />)
+
+  it("Should render correctly", () => {
+    expect(component).toMatchSnapshot()
+  })
+
+})

+ 13 - 0
packages/components/__tests__/SearchBar.test.js

@@ -0,0 +1,13 @@
+import React from "react"
+import { mount } from "enzyme"
+import SearchBar from "./../src/components/SearchBar"
+
+describe("SearchBar component", () => {
+
+  const component = mount(<SearchBar placeholder="Type here..." value="test" />)
+
+  it("Should render correctly", () => {
+    expect(component).toMatchSnapshot()
+  })
+
+})

+ 25 - 0
packages/components/__tests__/Tag.test.js

@@ -0,0 +1,25 @@
+import React from "react"
+import { mount } from "enzyme"
+import Tag from "./../src/components/Tag"
+import { faCheck } from "@fortawesome/free-solid-svg-icons"
+import { colors } from "./../src/theme"
+
+describe("Tag component", () => {
+
+  const component = mount(<Tag icon={faCheck} text="test" color={colors.other.success} />)
+
+  it("Should render correctly", () => {
+    expect(component).toMatchSnapshot()
+  })
+
+  it("Should render icon.", () => {
+    expect(component.find("svg"))
+      .toHaveLength(1)
+  })
+
+  it("Should render text.", () => {
+    expect(component.contains(<span>test</span>))
+      .toBe(true)
+  })
+
+})

+ 17 - 0
packages/components/__tests__/VideoPlayer.test.js

@@ -0,0 +1,17 @@
+import React from "react"
+import { mount } from "enzyme"
+import VideoPlayer from "./../src/components/VideoPlayer"
+
+describe("VideoPlayer component", () => {
+
+  const component = mount(
+    <VideoPlayer
+      src={"https://www.computt.com/asset/v0/5EjgEKNpyDbNdjcJoZ8izWuzeDUtAcaUvwG8vUWZQZ256NLb"}
+      poster={"https://ssl-static.libsyn.com/p/assets/a/4/8/f/a48f1a0697e958ce/Cover_2.png"}
+    />)
+
+  it("Should render correctly", () => {
+    expect(component).toMatchSnapshot()
+  })
+
+})

+ 17 - 0
packages/components/__tests__/VideoPreview.test.js

@@ -0,0 +1,17 @@
+import React from "react"
+import { mount } from "enzyme"
+import VideoPreview from "./../src/components/VideoPreview"
+
+describe("VideoPreview component", () => {
+
+  const component = mount(
+    <VideoPreview
+      title="Test"
+      poster="https://ssl-static.libsyn.com/p/assets/a/4/8/f/a48f1a0697e958ce/Cover_2.png"
+    />)
+
+  it("Should render correctly", () => {
+    expect(component).toMatchSnapshot()
+  })
+
+})

+ 28 - 0
packages/components/__tests__/__snapshots__/Avatar.test.js.snap

@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Avatar component Should render correctly 1`] = `
+<Memo(Avatar)
+  img="https://s3.amazonaws.com/keybase_processed_uploads/9003a57620356bd89d62bd34c7c0c305_360_360.jpg"
+>
+  <div
+    css={
+      Object {
+        "map": undefined,
+        "name": "9ax11r",
+        "next": undefined,
+        "styles": "
+    background-image: url(https://s3.amazonaws.com/keybase_processed_uploads/9003a57620356bd89d62bd34c7c0c305_360_360.jpg);
+    background-size: cover;
+    background-position: center;
+    border-radius: 50%;
+    min-width: 4.75rem;
+    min-height: 4.75rem;
+    max-width: 4.75rem;
+    max-height: 4.75rem;
+  ",
+        "toString": [Function],
+      }
+    }
+  />
+</Memo(Avatar)>
+`;

+ 25 - 0
packages/components/__tests__/__snapshots__/Banner.test.js.snap

@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Banner component Should render correctly 1`] = `
+<Memo(Banner)
+  src="http://static.libsyn.com/p/assets/2/c/2/5/2c25acab892a768e/Twitter_Cover.png"
+>
+  <div
+    css={
+      Object {
+        "map": undefined,
+        "name": "p5v1zq",
+        "next": undefined,
+        "styles": "
+    background-image: url(http://static.libsyn.com/p/assets/2/c/2/5/2c25acab892a768e/Twitter_Cover.png);
+    background-size: cover;
+    background-position: center;
+    width: 100%;
+    min-height: 6.25rem;
+  ",
+        "toString": [Function],
+      }
+    }
+  />
+</Memo(Banner)>
+`;

+ 70 - 0
packages/components/__tests__/__snapshots__/Button.test.js.snap

@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Button component Should render custom button correctly 1`] = `
+<Memo(Button)
+  color="success"
+  size="large"
+>
+  <button
+    css={
+      Object {
+        "map": undefined,
+        "name": "nynpfd",
+        "next": undefined,
+        "styles": "
+    
+    padding: 1rem 0.5rem;
+
+    font-size: 2rem;
+
+    width: auto;
+  ;
+    border-radius: 0.25rem;
+    border-color: ;
+    border-style: solid;
+    border-width: 2px;
+    font-style: 'Lato','Helvetica Neue',Arial,Helvetica,sans-serif;
+    background-color: #56BA46;
+    color: #fff;
+  ",
+        "toString": [Function],
+      }
+    }
+  >
+    Hello Atlas
+  </button>
+</Memo(Button)>
+`;
+
+exports[`Button component Should render default button correctly 1`] = `
+<Memo(Button)>
+  <button
+    css={
+      Object {
+        "map": undefined,
+        "name": "1s1uxvy",
+        "next": undefined,
+        "styles": "
+    
+    padding: 0.75rem 0.5rem;
+
+    font-size: 1rem;
+
+    width: auto;
+  ;
+    border-radius: 0.25rem;
+    border-color: ;
+    border-style: solid;
+    border-width: 2px;
+    font-style: 'Lato','Helvetica Neue',Arial,Helvetica,sans-serif;
+    background-color: #2578C2;
+    color: #fff;
+  ",
+        "toString": [Function],
+      }
+    }
+  >
+    Click me!
+  </button>
+</Memo(Button)>
+`;

+ 377 - 0
packages/components/__tests__/__snapshots__/ChannelSummary.test.js.snap

@@ -0,0 +1,377 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ChannelSummary component Should render correctly 1`] = `
+<Memo(ChannelSummary)
+  img="https://s3.amazonaws.com/keybase_processed_uploads/9003a57620356bd89d62bd34c7c0c305_360_360.jpg"
+  isPublic={true}
+  isVerified={true}
+  name="Test channel"
+  size="default"
+>
+  <div
+    css={
+      Object {
+        "map": undefined,
+        "name": "cbmrdi",
+        "next": undefined,
+        "styles": "
+      display: grid;
+      grid-template: auto / 4.75rem;
+      margin: 30px 0;
+    ",
+        "toString": [Function],
+      }
+    }
+  >
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "15zzzm1",
+          "next": undefined,
+          "styles": "
+      grid-column: 1 / 1;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      onClick={[Function]}
+    >
+      <Memo(Avatar)
+        img="https://s3.amazonaws.com/keybase_processed_uploads/9003a57620356bd89d62bd34c7c0c305_360_360.jpg"
+        size="default"
+      >
+        <div
+          css={
+            Object {
+              "map": undefined,
+              "name": "9ax11r",
+              "next": undefined,
+              "styles": "
+    background-image: url(https://s3.amazonaws.com/keybase_processed_uploads/9003a57620356bd89d62bd34c7c0c305_360_360.jpg);
+    background-size: cover;
+    background-position: center;
+    border-radius: 50%;
+    min-width: 4.75rem;
+    min-height: 4.75rem;
+    max-width: 4.75rem;
+    max-height: 4.75rem;
+  ",
+              "toString": [Function],
+            }
+          }
+        />
+      </Memo(Avatar)>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "1efshn",
+          "next": undefined,
+          "styles": "
+      grid-column: 2 / 2;
+      flex-direction: column;
+      margin: 0 0 0 1rem;
+      & > a {
+        text-decoration: none;
+      }
+    ",
+          "toString": [Function],
+        }
+      }
+    >
+      <h1
+        css={
+          Object {
+            "map": undefined,
+            "name": "1k3ibyh",
+            "next": undefined,
+            "styles": "
+      color: #2E86AB;
+      margin: 0.75rem 0;
+      display: inline-block;
+      cursor: pointer;
+    ",
+            "toString": [Function],
+          }
+        }
+        onClick={[Function]}
+      >
+        Test channel
+      </h1>
+      <div
+        css={
+          Object {
+            "map": undefined,
+            "name": "1t1yw03",
+            "next": undefined,
+            "styles": "
+      text-transform: uppercase;
+      font-size: 0.825rem;
+      & > *:not(last-child) {
+        margin-right: 0.5rem;
+      }
+    ",
+            "toString": [Function],
+          }
+        }
+      >
+        <Memo(Tag)
+          color="#219653"
+          icon={
+            Object {
+              "icon": Array [
+                512,
+                512,
+                Array [],
+                "f00c",
+                "M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z",
+              ],
+              "iconName": "check",
+              "prefix": "fas",
+            }
+          }
+          text="Verified"
+        >
+          <div
+            css={
+              Object {
+                "map": undefined,
+                "name": "1wt924n",
+                "next": undefined,
+                "styles": "
+      display: inline-block;
+      font-family: 'Lato','Helvetica Neue',Arial,Helvetica,sans-serif;
+      border: 1px solid #219653;
+      border-radius: 4px;
+      padding: 5px 10px;
+      color: #219653;
+      cursor: default;
+      vertical-align: middle;
+    ",
+                "toString": [Function],
+              }
+            }
+          >
+            <FontAwesomeIcon
+              border={false}
+              className=""
+              css={
+                Object {
+                  "map": undefined,
+                  "name": "1nzu8hs",
+                  "next": undefined,
+                  "styles": "
+      & > path:nth-of-type(1) {
+        color: inherit;
+        flex-shrink: 0;
+      }
+    ",
+                  "toString": [Function],
+                }
+              }
+              fixedWidth={false}
+              flip={null}
+              icon={
+                Object {
+                  "icon": Array [
+                    512,
+                    512,
+                    Array [],
+                    "f00c",
+                    "M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z",
+                  ],
+                  "iconName": "check",
+                  "prefix": "fas",
+                }
+              }
+              inverse={false}
+              listItem={false}
+              mask={null}
+              pull={null}
+              pulse={false}
+              rotation={null}
+              size={null}
+              spin={false}
+              swapOpacity={false}
+              symbol={false}
+              title=""
+              transform={null}
+            >
+              <svg
+                aria-hidden="true"
+                className="svg-inline--fa fa-check fa-w-16 "
+                css={
+                  Object {
+                    "map": undefined,
+                    "name": "1nzu8hs",
+                    "next": undefined,
+                    "styles": "
+      & > path:nth-of-type(1) {
+        color: inherit;
+        flex-shrink: 0;
+      }
+    ",
+                    "toString": [Function],
+                  }
+                }
+                data-icon="check"
+                data-prefix="fas"
+                focusable="false"
+                role="img"
+                style={Object {}}
+                viewBox="0 0 512 512"
+                xmlns="http://www.w3.org/2000/svg"
+              >
+                <path
+                  d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"
+                  fill="currentColor"
+                  style={Object {}}
+                />
+              </svg>
+            </FontAwesomeIcon>
+            <span
+              style={
+                Object {
+                  "margin": "0 10px 0 0",
+                }
+              }
+            />
+            <span>
+              Verified
+            </span>
+          </div>
+        </Memo(Tag)>
+        <Memo(Tag)
+          color="#2F80ED"
+          icon={
+            Object {
+              "icon": Array [
+                576,
+                512,
+                Array [],
+                "f06e",
+                "M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z",
+              ],
+              "iconName": "eye",
+              "prefix": "fas",
+            }
+          }
+          text="Public"
+        >
+          <div
+            css={
+              Object {
+                "map": undefined,
+                "name": "kqccd5",
+                "next": undefined,
+                "styles": "
+      display: inline-block;
+      font-family: 'Lato','Helvetica Neue',Arial,Helvetica,sans-serif;
+      border: 1px solid #2F80ED;
+      border-radius: 4px;
+      padding: 5px 10px;
+      color: #2F80ED;
+      cursor: default;
+      vertical-align: middle;
+    ",
+                "toString": [Function],
+              }
+            }
+          >
+            <FontAwesomeIcon
+              border={false}
+              className=""
+              css={
+                Object {
+                  "map": undefined,
+                  "name": "1nzu8hs",
+                  "next": undefined,
+                  "styles": "
+      & > path:nth-of-type(1) {
+        color: inherit;
+        flex-shrink: 0;
+      }
+    ",
+                  "toString": [Function],
+                }
+              }
+              fixedWidth={false}
+              flip={null}
+              icon={
+                Object {
+                  "icon": Array [
+                    576,
+                    512,
+                    Array [],
+                    "f06e",
+                    "M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z",
+                  ],
+                  "iconName": "eye",
+                  "prefix": "fas",
+                }
+              }
+              inverse={false}
+              listItem={false}
+              mask={null}
+              pull={null}
+              pulse={false}
+              rotation={null}
+              size={null}
+              spin={false}
+              swapOpacity={false}
+              symbol={false}
+              title=""
+              transform={null}
+            >
+              <svg
+                aria-hidden="true"
+                className="svg-inline--fa fa-eye fa-w-18 "
+                css={
+                  Object {
+                    "map": undefined,
+                    "name": "1nzu8hs",
+                    "next": undefined,
+                    "styles": "
+      & > path:nth-of-type(1) {
+        color: inherit;
+        flex-shrink: 0;
+      }
+    ",
+                    "toString": [Function],
+                  }
+                }
+                data-icon="eye"
+                data-prefix="fas"
+                focusable="false"
+                role="img"
+                style={Object {}}
+                viewBox="0 0 576 512"
+                xmlns="http://www.w3.org/2000/svg"
+              >
+                <path
+                  d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"
+                  fill="currentColor"
+                  style={Object {}}
+                />
+              </svg>
+            </FontAwesomeIcon>
+            <span
+              style={
+                Object {
+                  "margin": "0 10px 0 0",
+                }
+              }
+            />
+            <span>
+              Public
+            </span>
+          </div>
+        </Memo(Tag)>
+      </div>
+    </div>
+  </div>
+</Memo(ChannelSummary)>
+`;

+ 334 - 0
packages/components/__tests__/__snapshots__/DetailsTable.test.js.snap

@@ -0,0 +1,334 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DetailsTable component Should render correctly 1`] = `
+<Memo(DetailsTable)
+  details={
+    Object {
+      "attribution": "",
+      "category": "Science & Technology",
+      "explicit": "yes",
+      "first released": "2019-02-27",
+      "language": "English",
+      "license": "Original content",
+    }
+  }
+>
+  <table
+    css={
+      Object {
+        "map": undefined,
+        "name": "1eb017w",
+        "next": undefined,
+        "styles": "
+      text-align: left;
+      border-collapse: separate;
+      border-spacing: 0;
+      text-transform: capitalize;
+    ",
+        "toString": [Function],
+      }
+    }
+  >
+    <tbody>
+      <tr
+        css={
+          Object {
+            "map": undefined,
+            "name": "oc1vdi",
+            "next": undefined,
+            "styles": "
+      td {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+      }
+    ",
+            "toString": [Function],
+          }
+        }
+        key="explicit"
+      >
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "uva6ry",
+              "next": undefined,
+              "styles": "
+      font-weight: 700;
+      padding: 0.5rem 0.75rem;
+      width: 25%;
+      color: #999;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          explicit
+        </td>
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "naho03",
+              "next": undefined,
+              "styles": "
+      padding: 0.5rem 0.75rem;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          yes
+        </td>
+      </tr>
+      <tr
+        css={
+          Object {
+            "map": undefined,
+            "name": "oc1vdi",
+            "next": undefined,
+            "styles": "
+      td {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+      }
+    ",
+            "toString": [Function],
+          }
+        }
+        key="first released"
+      >
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "uva6ry",
+              "next": undefined,
+              "styles": "
+      font-weight: 700;
+      padding: 0.5rem 0.75rem;
+      width: 25%;
+      color: #999;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          first released
+        </td>
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "naho03",
+              "next": undefined,
+              "styles": "
+      padding: 0.5rem 0.75rem;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          2019-02-27
+        </td>
+      </tr>
+      <tr
+        css={
+          Object {
+            "map": undefined,
+            "name": "oc1vdi",
+            "next": undefined,
+            "styles": "
+      td {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+      }
+    ",
+            "toString": [Function],
+          }
+        }
+        key="language"
+      >
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "uva6ry",
+              "next": undefined,
+              "styles": "
+      font-weight: 700;
+      padding: 0.5rem 0.75rem;
+      width: 25%;
+      color: #999;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          language
+        </td>
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "naho03",
+              "next": undefined,
+              "styles": "
+      padding: 0.5rem 0.75rem;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          English
+        </td>
+      </tr>
+      <tr
+        css={
+          Object {
+            "map": undefined,
+            "name": "oc1vdi",
+            "next": undefined,
+            "styles": "
+      td {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+      }
+    ",
+            "toString": [Function],
+          }
+        }
+        key="category"
+      >
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "uva6ry",
+              "next": undefined,
+              "styles": "
+      font-weight: 700;
+      padding: 0.5rem 0.75rem;
+      width: 25%;
+      color: #999;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          category
+        </td>
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "naho03",
+              "next": undefined,
+              "styles": "
+      padding: 0.5rem 0.75rem;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          Science & Technology
+        </td>
+      </tr>
+      <tr
+        css={
+          Object {
+            "map": undefined,
+            "name": "oc1vdi",
+            "next": undefined,
+            "styles": "
+      td {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+      }
+    ",
+            "toString": [Function],
+          }
+        }
+        key="license"
+      >
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "uva6ry",
+              "next": undefined,
+              "styles": "
+      font-weight: 700;
+      padding: 0.5rem 0.75rem;
+      width: 25%;
+      color: #999;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          license
+        </td>
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "naho03",
+              "next": undefined,
+              "styles": "
+      padding: 0.5rem 0.75rem;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          Original content
+        </td>
+      </tr>
+      <tr
+        css={
+          Object {
+            "map": undefined,
+            "name": "oc1vdi",
+            "next": undefined,
+            "styles": "
+      td {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+      }
+    ",
+            "toString": [Function],
+          }
+        }
+        key="attribution"
+      >
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "uva6ry",
+              "next": undefined,
+              "styles": "
+      font-weight: 700;
+      padding: 0.5rem 0.75rem;
+      width: 25%;
+      color: #999;
+    ",
+              "toString": [Function],
+            }
+          }
+        >
+          attribution
+        </td>
+        <td
+          css={
+            Object {
+              "map": undefined,
+              "name": "naho03",
+              "next": undefined,
+              "styles": "
+      padding: 0.5rem 0.75rem;
+    ",
+              "toString": [Function],
+            }
+          }
+        />
+      </tr>
+    </tbody>
+  </table>
+</Memo(DetailsTable)>
+`;

+ 300 - 0
packages/components/__tests__/__snapshots__/Grid.test.js.snap

@@ -0,0 +1,300 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Grid component Should render correctly 1`] = `
+<Memo(Grid)
+  items={
+    Array [
+      <Item
+        text="test-0"
+      />,
+      <Item
+        text="test-1"
+      />,
+      <Item
+        text="test-2"
+      />,
+      <Item
+        text="test-3"
+      />,
+      <Item
+        text="test-4"
+      />,
+      <Item
+        text="test-5"
+      />,
+      <Item
+        text="test-6"
+      />,
+      <Item
+        text="test-7"
+      />,
+      <Item
+        text="test-8"
+      />,
+      <Item
+        text="test-9"
+      />,
+    ]
+  }
+  maxItemWidth="600"
+  minItemWidth="250"
+>
+  <div
+    className=""
+    css={
+      Object {
+        "map": undefined,
+        "name": "1sl9x6x",
+        "next": undefined,
+        "styles": "
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(250px, 600px));
+      gap: 30px;
+    ",
+        "toString": [Function],
+      }
+    }
+  >
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-0"
+    >
+      <Item
+        key="0"
+        text="test-0"
+      >
+        <div>
+          test-0
+        </div>
+      </Item>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-1"
+    >
+      <Item
+        key="1"
+        text="test-1"
+      >
+        <div>
+          test-1
+        </div>
+      </Item>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-2"
+    >
+      <Item
+        key="2"
+        text="test-2"
+      >
+        <div>
+          test-2
+        </div>
+      </Item>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-3"
+    >
+      <Item
+        key="3"
+        text="test-3"
+      >
+        <div>
+          test-3
+        </div>
+      </Item>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-4"
+    >
+      <Item
+        key="4"
+        text="test-4"
+      >
+        <div>
+          test-4
+        </div>
+      </Item>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-5"
+    >
+      <Item
+        key="5"
+        text="test-5"
+      >
+        <div>
+          test-5
+        </div>
+      </Item>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-6"
+    >
+      <Item
+        key="6"
+        text="test-6"
+      >
+        <div>
+          test-6
+        </div>
+      </Item>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-7"
+    >
+      <Item
+        key="7"
+        text="test-7"
+      >
+        <div>
+          test-7
+        </div>
+      </Item>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-8"
+    >
+      <Item
+        key="8"
+        text="test-8"
+      >
+        <div>
+          test-8
+        </div>
+      </Item>
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "wuoxs",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      cursor: pointer;
+    ",
+          "toString": [Function],
+        }
+      }
+      key="grid-item-9"
+    >
+      <Item
+        key="9"
+        text="test-9"
+      >
+        <div>
+          test-9
+        </div>
+      </Item>
+    </div>
+  </div>
+</Memo(Grid)>
+`;

+ 84 - 0
packages/components/__tests__/__snapshots__/SearchBar.test.js.snap

@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SearchBar component Should render correctly 1`] = `
+<Memo(SearchBar)
+  placeholder="Type here..."
+  value="test"
+>
+  <div
+    css={
+      Object {
+        "map": undefined,
+        "name": "laoupi",
+        "next": undefined,
+        "styles": "
+      display: flex;
+      padding: 1.5rem;
+      margin: 1rem;
+      border-radius: 0.5rem;
+      border: none;
+
+      & * {
+        font-size: 1rem;
+      }
+    ",
+        "toString": [Function],
+      }
+    }
+  >
+    <input
+      css={
+        Object {
+          "map": undefined,
+          "name": "c0kzty",
+          "next": undefined,
+          "styles": "
+      padding: 0.75rem 0.5rem;
+      font-weight: 100;
+      border-radius: 0.25rem;
+      border: 1px solid #000;
+    ",
+          "toString": [Function],
+        }
+      }
+      onChange={[Function]}
+      placeholder="Type here..."
+      type="text"
+      value="test"
+    />
+    <Memo(Button)
+      onClick={[Function]}
+    >
+      <button
+        css={
+          Object {
+            "map": undefined,
+            "name": "1s1uxvy",
+            "next": undefined,
+            "styles": "
+    
+    padding: 0.75rem 0.5rem;
+
+    font-size: 1rem;
+
+    width: auto;
+  ;
+    border-radius: 0.25rem;
+    border-color: ;
+    border-style: solid;
+    border-width: 2px;
+    font-style: 'Lato','Helvetica Neue',Arial,Helvetica,sans-serif;
+    background-color: #2578C2;
+    color: #fff;
+  ",
+            "toString": [Function],
+          }
+        }
+        onClick={[Function]}
+      >
+        Search
+      </button>
+    </Memo(Button)>
+  </div>
+</Memo(SearchBar)>
+`;

+ 130 - 0
packages/components/__tests__/__snapshots__/Tag.test.js.snap

@@ -0,0 +1,130 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tag component Should render correctly 1`] = `
+<Memo(Tag)
+  color="#219653"
+  icon={
+    Object {
+      "icon": Array [
+        512,
+        512,
+        Array [],
+        "f00c",
+        "M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z",
+      ],
+      "iconName": "check",
+      "prefix": "fas",
+    }
+  }
+  text="test"
+>
+  <div
+    css={
+      Object {
+        "map": undefined,
+        "name": "1wt924n",
+        "next": undefined,
+        "styles": "
+      display: inline-block;
+      font-family: 'Lato','Helvetica Neue',Arial,Helvetica,sans-serif;
+      border: 1px solid #219653;
+      border-radius: 4px;
+      padding: 5px 10px;
+      color: #219653;
+      cursor: default;
+      vertical-align: middle;
+    ",
+        "toString": [Function],
+      }
+    }
+  >
+    <FontAwesomeIcon
+      border={false}
+      className=""
+      css={
+        Object {
+          "map": undefined,
+          "name": "1nzu8hs",
+          "next": undefined,
+          "styles": "
+      & > path:nth-of-type(1) {
+        color: inherit;
+        flex-shrink: 0;
+      }
+    ",
+          "toString": [Function],
+        }
+      }
+      fixedWidth={false}
+      flip={null}
+      icon={
+        Object {
+          "icon": Array [
+            512,
+            512,
+            Array [],
+            "f00c",
+            "M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z",
+          ],
+          "iconName": "check",
+          "prefix": "fas",
+        }
+      }
+      inverse={false}
+      listItem={false}
+      mask={null}
+      pull={null}
+      pulse={false}
+      rotation={null}
+      size={null}
+      spin={false}
+      swapOpacity={false}
+      symbol={false}
+      title=""
+      transform={null}
+    >
+      <svg
+        aria-hidden="true"
+        className="svg-inline--fa fa-check fa-w-16 "
+        css={
+          Object {
+            "map": undefined,
+            "name": "1nzu8hs",
+            "next": undefined,
+            "styles": "
+      & > path:nth-of-type(1) {
+        color: inherit;
+        flex-shrink: 0;
+      }
+    ",
+            "toString": [Function],
+          }
+        }
+        data-icon="check"
+        data-prefix="fas"
+        focusable="false"
+        role="img"
+        style={Object {}}
+        viewBox="0 0 512 512"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <path
+          d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"
+          fill="currentColor"
+          style={Object {}}
+        />
+      </svg>
+    </FontAwesomeIcon>
+    <span
+      style={
+        Object {
+          "margin": "0 10px 0 0",
+        }
+      }
+    />
+    <span>
+      test
+    </span>
+  </div>
+</Memo(Tag)>
+`;

+ 148 - 0
packages/components/__tests__/__snapshots__/VideoPlayer.test.js.snap

@@ -0,0 +1,148 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VideoPlayer component Should render correctly 1`] = `
+<Memo(VideoPlayer)
+  poster="https://ssl-static.libsyn.com/p/assets/a/4/8/f/a48f1a0697e958ce/Cover_2.png"
+  src="https://www.computt.com/asset/v0/5EjgEKNpyDbNdjcJoZ8izWuzeDUtAcaUvwG8vUWZQZ256NLb"
+>
+  <div
+    css={
+      Object {
+        "map": undefined,
+        "name": "19dglr4",
+        "next": undefined,
+        "styles": "
+      max-width: 48rem;
+      & .video-player {
+      }
+    ",
+        "toString": [Function],
+      }
+    }
+  >
+    <ReactPlayer
+      config={
+        Object {
+          "file": Object {
+            "attributes": Object {
+              "className": "video-player",
+            },
+          },
+        }
+      }
+      controls={true}
+      css={
+        Object {
+          "map": undefined,
+          "name": "bggj8i",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      height: 100%;
+    ",
+          "toString": [Function],
+        }
+      }
+      height="360px"
+      light="https://ssl-static.libsyn.com/p/assets/a/4/8/f/a48f1a0697e958ce/Cover_2.png"
+      loop={false}
+      muted={false}
+      onBuffer={[Function]}
+      onBufferEnd={[Function]}
+      onDisablePIP={[Function]}
+      onDuration={[Function]}
+      onEnablePIP={[Function]}
+      onEnded={[Function]}
+      onError={[Function]}
+      onPause={[Function]}
+      onPlay={[Function]}
+      onProgress={[Function]}
+      onReady={[Function]}
+      onSeek={[Function]}
+      onStart={[Function]}
+      pip={false}
+      playbackRate={1}
+      playing={false}
+      playsinline={false}
+      progressInterval={1000}
+      style={Object {}}
+      url="https://www.computt.com/asset/v0/5EjgEKNpyDbNdjcJoZ8izWuzeDUtAcaUvwG8vUWZQZ256NLb"
+      volume={null}
+      width="640px"
+      wrapper="div"
+    >
+      <div
+        css={
+          Object {
+            "map": undefined,
+            "name": "bggj8i",
+            "next": undefined,
+            "styles": "
+      width: 100%;
+      height: 100%;
+    ",
+            "toString": [Function],
+          }
+        }
+        style={
+          Object {
+            "height": "360px",
+            "width": "640px",
+          }
+        }
+      >
+        <Preview
+          light="https://ssl-static.libsyn.com/p/assets/a/4/8/f/a48f1a0697e958ce/Cover_2.png"
+          onClick={[Function]}
+          url="https://www.computt.com/asset/v0/5EjgEKNpyDbNdjcJoZ8izWuzeDUtAcaUvwG8vUWZQZ256NLb"
+        >
+          <div
+            className="react-player__preview"
+            onClick={[Function]}
+            style={
+              Object {
+                "alignItems": "center",
+                "backgroundImage": "url(https://ssl-static.libsyn.com/p/assets/a/4/8/f/a48f1a0697e958ce/Cover_2.png)",
+                "backgroundPosition": "center",
+                "backgroundSize": "cover",
+                "cursor": "pointer",
+                "display": "flex",
+                "height": "100%",
+                "justifyContent": "center",
+                "width": "100%",
+              }
+            }
+          >
+            <div
+              className="react-player__shadow"
+              style={
+                Object {
+                  "alignItems": "center",
+                  "background": "radial-gradient(rgb(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 60%)",
+                  "borderRadius": "64px",
+                  "display": "flex",
+                  "height": "64px",
+                  "justifyContent": "center",
+                  "width": "64px",
+                }
+              }
+            >
+              <div
+                className="react-player__play-icon"
+                style={
+                  Object {
+                    "borderColor": "transparent transparent transparent white",
+                    "borderStyle": "solid",
+                    "borderWidth": "16px 0 16px 26px",
+                    "marginLeft": "7px",
+                  }
+                }
+              />
+            </div>
+          </div>
+        </Preview>
+      </div>
+    </ReactPlayer>
+  </div>
+</Memo(VideoPlayer)>
+`;

+ 103 - 0
packages/components/__tests__/__snapshots__/VideoPreview.test.js.snap

@@ -0,0 +1,103 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VideoPreview component Should render correctly 1`] = `
+<Memo(VideoPreview)
+  poster="https://ssl-static.libsyn.com/p/assets/a/4/8/f/a48f1a0697e958ce/Cover_2.png"
+  title="Test"
+>
+  <div
+    css={
+      Object {
+        "map": undefined,
+        "name": "0",
+        "next": undefined,
+        "styles": "",
+        "toString": [Function],
+      }
+    }
+  >
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "fqj6m7",
+          "next": undefined,
+          "styles": "
+      width: 100%;
+      background-color: black;
+    ",
+          "toString": [Function],
+        }
+      }
+    >
+      <img
+        css={
+          Object {
+            "map": undefined,
+            "name": "1lk1tws",
+            "next": undefined,
+            "styles": "
+      display: block;
+      width: 100%;
+      height: auto;
+    ",
+            "toString": [Function],
+          }
+        }
+        onClick={[Function]}
+        src="https://ssl-static.libsyn.com/p/assets/a/4/8/f/a48f1a0697e958ce/Cover_2.png"
+      />
+    </div>
+    <div
+      css={
+        Object {
+          "map": undefined,
+          "name": "1r27l9a",
+          "next": undefined,
+          "styles": "
+      display: grid;
+      grid-template: auto / auto;
+      margin: 10px 0 0;
+    ",
+          "toString": [Function],
+        }
+      }
+    >
+      <div
+        css={
+          Object {
+            "map": undefined,
+            "name": "opyjht",
+            "next": undefined,
+            "styles": "
+      grid-column: 1 / 1;
+    ",
+            "toString": [Function],
+          }
+        }
+      >
+        <h3
+          css={
+            Object {
+              "map": undefined,
+              "name": "1yuwxk0",
+              "next": undefined,
+              "styles": "
+      margin: 0;
+      font-weight: 700;
+      text-transform: capitalize;
+      color: #000;
+      font-size: 0.825rem;
+    ",
+              "toString": [Function],
+            }
+          }
+          onClick={[Function]}
+        >
+          Test
+        </h3>
+      </div>
+    </div>
+  </div>
+</Memo(VideoPreview)>
+`;

+ 8 - 0
packages/components/babel.config.json

@@ -0,0 +1,8 @@
+{
+  "presets": [
+    "@babel/preset-env",
+    "@babel/preset-react",
+    "@babel/preset-typescript",
+    "@emotion/babel-preset-css-prop"
+  ]
+}

+ 11 - 0
packages/components/now.json

@@ -0,0 +1,11 @@
+{
+  "builds": [
+    {
+      "src": "package.json",
+      "use": "@now/static-build",
+      "config": {
+        "distDir": "storybook-static"
+      }
+    }
+  ]
+}

+ 67 - 0
packages/components/package.json

@@ -0,0 +1,67 @@
+{
+  "name": "components",
+  "version": "1.0.0",
+  "description": "React Components for the Atlas Project",
+  "homepage": "https://github.com/Joystream/atlas#readme",
+  "license": "ISC",
+  "main": "dist/index.cjs.js",
+  "module": "dist/index.es.js",
+  "types": "dist/types",
+  "directories": {
+    "src": "src",
+    "test": "__tests__"
+  },
+  "files": [
+    "src"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/Joystream/atlas.git"
+  },
+  "scripts": {
+    "start": "rollup -wc",
+    "index": "node scripts/build-index.js",
+    "build": "rollup -c",
+    "storybook": "start-storybook -p 6006",
+    "build-storybook": "build-storybook",
+    "now-build": "build-storybook",
+    "test": "echo \"Error: run tests from root\" && exit 1"
+  },
+  "bugs": {
+    "url": "https://github.com/Joystream/atlas/issues"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.8.7",
+    "@babel/preset-env": "^7.8.7",
+    "@babel/preset-react": "^7.8.3",
+    "@babel/preset-typescript": "^7.9.0",
+    "@emotion/babel-preset-css-prop": "^10.0.27",
+    "@emotion/core": "^10.0.28",
+    "@fortawesome/fontawesome-svg-core": "^1.2.28",
+    "@fortawesome/free-solid-svg-icons": "^5.13.0",
+    "@fortawesome/react-fontawesome": "^0.1.9",
+    "@rollup/plugin-commonjs": "^11.0.2",
+    "@rollup/plugin-node-resolve": "^7.1.1",
+    "@storybook/addon-actions": "^5.3.17",
+    "@storybook/addon-docs": "^5.3.17",
+    "@storybook/addon-knobs": "^5.3.17",
+    "@storybook/addon-links": "^5.3.17",
+    "@storybook/addon-storysource": "^5.3.17",
+    "@storybook/addons": "^5.3.17",
+    "@storybook/react": "^5.3.17",
+    "babel-jest": "^25.4.0",
+    "babel-loader": "^8.0.6",
+    "babel-plugin-transform-export-extensions": "^6.22.0",
+    "react": "^16.13.0",
+    "react-docgen-typescript-loader": "^3.7.1",
+    "react-dom": "^16.13.0",
+    "react-player": "^1.15.3",
+    "rollup": "^2.1.0",
+    "rollup-plugin-babel": "^4.4.0",
+    "rollup-plugin-typescript2": "^0.26.0",
+    "storybook-addon-jsx": "^7.1.15",
+    "ts-loader": "^6.2.1",
+    "typescript": "^3.8.3",
+    "video-react": "^0.14.1"
+  }
+}

+ 44 - 0
packages/components/rollup.config.js

@@ -0,0 +1,44 @@
+import resolve from "@rollup/plugin-node-resolve";
+import commonjs from "@rollup/plugin-commonjs";
+import typescript from "rollup-plugin-typescript2";
+import babel from "rollup-plugin-babel";
+import { DEFAULT_EXTENSIONS } from "@babel/core";
+import react from "react";
+import reactDom from "react-dom";
+
+import pkg from "./package.json";
+
+export default {
+  input: "src/index.ts",
+  output: [
+    {
+      file: pkg.main,
+      format: "cjs",
+      file: pkg.main,
+    },
+    {
+      format: "esm",
+      file: pkg.module,
+    },
+  ],
+  plugins: [
+    resolve({
+      extensions: [".js", ".jsx", ".ts", ".tsx"],
+      preferBuiltins: true,
+    }),
+    commonjs({
+      exclude: "../../node_modules",
+      namedExports: {
+        react: Object.keys(react),
+        "react-dom": Object.keys(reactDom),
+      },
+    }),
+    typescript({
+      useTsconfigDeclarationDir: true,
+    }),
+    babel({
+      exclude: ["../../node_modules/**", "node_modules/**"],
+      extensions: [...DEFAULT_EXTENSIONS, ".ts", ".tsx"],
+    }),
+  ],
+};

+ 17 - 0
packages/components/scripts/build-index.js

@@ -0,0 +1,17 @@
+const fs = require("fs");
+const path = require("path");
+
+const componentsPath = path.resolve(__dirname, "../src/components");
+const indexPath = path.resolve(__dirname, "../src");
+
+let statements = fs
+  .readdirSync(componentsPath)
+  .map(file => {
+    let filePath = path.resolve(componentsPath, file);
+    let relPath = path.relative(indexPath, filePath);
+    let { dir, name } = path.parse(relPath);
+    return `export { default as ${name} } from \"./${dir}/${name}\";`;
+  })
+  .join("\n");
+
+fs.writeFileSync(path.resolve(indexPath, "index.ts"), statements);

+ 28 - 0
packages/components/src/components/Avatar/Avatar.style.tsx

@@ -0,0 +1,28 @@
+import { css } from "@emotion/core"
+import { spacing, colors } from "../../theme"
+
+export type AvatarStyleProps = {
+  img?: string
+  size?: "small" | "default" | "large"
+}
+
+export let makeStyles = ({ img, size = "default" }: AvatarStyleProps) => {
+  let width =
+    size === "small"
+      ? spacing.s9
+      : size === "default"
+      ? spacing.s19
+      : spacing.s25;
+  return css`
+    background-image: ${img
+      ? `url(${img})`
+      : `radial-gradient(${colors.bg.primary}, ${colors.bg.primary})`};
+    background-size: cover;
+    background-position: center;
+    border-radius: 50%;
+    min-width: ${width};
+    min-height: ${width};
+    max-width: ${width};
+    max-height: ${width};
+  `
+}

+ 11 - 0
packages/components/src/components/Avatar/Avatar.tsx

@@ -0,0 +1,11 @@
+import React from "react";
+import { makeStyles, AvatarStyleProps } from "./Avatar.style";
+
+export type AvatarProps = {} & AvatarStyleProps;
+
+export default function Avatar({ ...styleProps }: AvatarProps) {
+  let styles = makeStyles(styleProps);
+  return (
+    <div css={styles} />
+  );
+}

+ 3 - 0
packages/components/src/components/Avatar/index.tsx

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import Avatar from "./Avatar"
+export default memo(Avatar)

+ 157 - 0
packages/components/src/components/Button/Button.style.ts

@@ -0,0 +1,157 @@
+import { css } from "@emotion/core"
+import { typography, colors } from "../../theme"
+
+export type ButtonStyleProps = {
+  text?: string
+  type?: "primary" | "secondary"
+  width?: "normal" | "full"
+  size?: "regular" | "small" | "smaller"
+  disabled?: boolean
+}
+
+export let makeStyles = ({
+  text,
+  type = "primary",
+  width = "normal",
+  size = "regular",
+  disabled = false
+}: ButtonStyleProps) => {
+
+  const buttonHeight = size === "regular" ? "20px" : size === "small" ? "15px" : "10px";
+
+  const primaryButton = {
+    container: css`
+      border: 1px solid ${colors.blue[500]};
+      color: ${colors.white};
+      background-color: ${colors.blue[500]};
+      justify-content:center;
+      padding: ${size === "regular" ? (!!text ? "14px 17px" : "14px") :
+        size === "small" ? (!!text ? "12px 14px" : "12px") : "10px"
+      };
+      display: ${width === "normal" ? "inline-flex" : "flex"};
+      align-items: center;
+      cursor: default;
+      font-family: ${typography.fonts.base};
+      font-weight: ${typography.weights.medium};
+      font-size: ${size === "regular" ? typography.sizes.button.large :
+        size === "small" ? typography.sizes.button.medium : 
+        typography.sizes.button.small
+      };
+      margin: 0 ${width === "normal" ? "15px" : "0"} 0 0;
+      height: ${buttonHeight};
+      max-height: ${buttonHeight};
+
+      &:hover {
+        background-color: ${colors.blue[700]};
+        border-color: ${colors.blue[700]};
+        color: ${colors.white};
+      }
+
+      &:active {
+        background-color: ${colors.blue[900]};
+        border-color: ${colors.blue[900]};
+        color: ${colors.white};
+      }
+
+      &::selection {
+        background: transparent;
+      }
+    `
+  }
+
+  const secondaryButton = {
+    container: css`
+      border: 1px solid ${colors.blue[500]};
+      color: ${colors.white};
+      background-color: ${colors.black};
+      justify-content:center;
+      padding: ${size === "regular" ? (!!text ? "14px 17px" : "14px") :
+        size === "small" ? (!!text ? "12px 14px" : "12px") : "10px"
+      };
+      display: ${width === "normal" ? "inline-flex" : "flex"};
+      align-items: center;
+      cursor: default;
+      font-family: ${typography.fonts.base};
+      font-weight: ${typography.weights.medium};
+      font-size: ${size === "regular" ? typography.sizes.button.large :
+        size === "small" ? typography.sizes.button.medium : 
+        typography.sizes.button.small
+      };
+      margin: 0 ${width === "normal" ? "15px" : "0"} 0 0;
+      height: ${buttonHeight};
+      max-height: ${buttonHeight};
+
+      &:hover {
+        background-color: ${colors.black};
+        border-color: ${colors.blue[700]};
+        color: ${colors.blue[300]};
+      }
+
+      &:active {
+        background-color: ${colors.black};
+        border-color: ${colors.blue[700]};
+        color: ${colors.blue[700]};
+      }
+
+      &::selection {
+        background: transparent;
+      }
+    `
+  }
+
+  const disabledButton = {
+    container: css`
+      border: 1px solid ${colors.white};
+      color: ${colors.white};
+      background-color: ${colors.gray[100]};
+      justify-content:center;
+      padding: ${size === "regular" ? (!!text ? "14px 17px" : "14px") :
+        size === "small" ? (!!text ? "12px 14px" : "12px") : "10px"
+      };
+      display: ${width === "normal" ? "inline-flex" : "flex"};
+      align-items: center;
+      cursor: ${disabled ? "not-allowed" : "default"};
+      font-family: ${typography.fonts.base};
+      font-weight: ${typography.weights.medium};
+      font-size: ${size === "regular" ? typography.sizes.button.large :
+        size === "small" ? typography.sizes.button.medium : 
+        typography.sizes.button.small
+      };
+      margin: 0 ${width === "normal" ? "15px" : "0"} 0 0;
+      height: ${buttonHeight};
+      max-height: ${buttonHeight};
+
+      &:hover {
+        background-color: ${colors.gray[100]};
+        border-color: ${colors.white};
+        color: ${colors.white};
+      }
+
+      &:active {
+        background-color: ${colors.gray[100]};
+        border-color: ${colors.white};
+        color: ${colors.white};
+      }
+
+      &::selection {
+        background: transparent;
+      }
+    `
+  }
+
+  const icon = css`
+    margin-right: ${!!text ? "10px" : "0"};
+    font-size: ${size === "regular" ? typography.sizes.icon.large :
+        size === "small" ? typography.sizes.icon.medium : 
+        typography.sizes.icon.small
+      };
+
+    & > path:nth-of-type(1) {	
+      color: inherit;	
+      flex-shrink: 0;
+    }
+  `
+
+  const result = disabled ? disabledButton : type === "primary" ? primaryButton : secondaryButton
+  return { icon, ...result }
+}

+ 29 - 0
packages/components/src/components/Button/Button.tsx

@@ -0,0 +1,29 @@
+import React from "react"
+import { makeStyles, ButtonStyleProps } from "./Button.style"
+import { IconProp } from "@fortawesome/fontawesome-svg-core"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+
+type ButtonProps = {
+  text?: string
+  icon?: IconProp
+  disabled?: boolean
+  onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
+} & ButtonStyleProps
+
+export default function Button({
+  text = "",
+  icon,
+  disabled = false,
+  onClick,
+  ...styleProps
+}: ButtonProps) {
+  let styles = makeStyles({text, disabled, ...styleProps})
+  return (
+    <div css={styles.container} onClick={disabled ? null : onClick}>
+      {!!icon &&
+        <FontAwesomeIcon icon={icon} css={styles.icon} />
+      }
+      {text}
+    </div>
+  )
+}

+ 3 - 0
packages/components/src/components/Button/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import Button from "./Button"
+export default memo(Button)

+ 23 - 0
packages/components/src/components/Grid/Grid.style.ts

@@ -0,0 +1,23 @@
+import { css } from "@emotion/core"
+
+export type GridStyleProps = {
+  minItemWidth?: string | number
+  maxItemWidth?: string | number
+}
+
+export let makeStyles = ({
+  minItemWidth = "300",
+  maxItemWidth
+}: GridStyleProps) => {
+  return {
+    container: css`
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(${minItemWidth}px, ${maxItemWidth ? maxItemWidth + "px" : "1fr"}));
+      gap: 30px;
+    `,
+    item: css`
+      width: 100%;
+      cursor: pointer;
+    `,
+  }
+}

+ 20 - 0
packages/components/src/components/Grid/Grid.tsx

@@ -0,0 +1,20 @@
+import React from "react"
+import { makeStyles, GridStyleProps } from "./Grid.style"
+
+type SectionProps = {
+  items?: React.ReactNode[]
+  className?: string
+} & GridStyleProps
+
+export default function Grid({
+  items = [],
+  className = "",
+  ...styleProps
+}: SectionProps) {
+  let styles = makeStyles(styleProps)
+  return (
+    <div css={styles.container} className={className}>
+      {items.map((item, index) => <div key={`grid-item-${index}`} css={styles.item}>{item}</div>)}
+    </div>
+  )
+}

+ 3 - 0
packages/components/src/components/Grid/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import Grid from "./Grid"
+export default memo(Grid)

+ 30 - 0
packages/components/src/components/Header/Header.style.ts

@@ -0,0 +1,30 @@
+import { css } from "@emotion/core"
+import { typography, colors } from "../../theme"
+
+export type HeaderStyleProps = {
+  background?: string
+}
+
+export let makeStyles = ({
+  background = ""
+}: HeaderStyleProps) => {
+  return css`
+    background-color: ${colors.black};
+    text-align: left;
+    cursor: default;
+    color: ${colors.white};
+    font-family: ${typography.fonts.base};
+    height: 600px;
+    display: flex;
+    align-content: center;
+    align-items: center;
+    background-image: url(${background});
+    background-repeat: no-repeat;
+    background-position: center right;
+    background-size: contain;
+
+    div#content {
+      margin: 0 100px;
+    }   
+  `
+}

+ 32 - 0
packages/components/src/components/Header/Header.tsx

@@ -0,0 +1,32 @@
+import React, { Children } from "react"
+import { makeStyles, HeaderStyleProps } from "./Header.style"
+
+type HeaderProps = {
+  text: string,
+  subtext?: string,
+  children?: React.ReactNode
+} & HeaderStyleProps
+
+export default function Header({
+  text,
+  subtext = "",
+  children,
+  ...styleProps
+}: HeaderProps) {
+  let styles = makeStyles(styleProps)
+  return (
+    <div css={styles}>
+      <div id="content">
+        <h1>
+          {text}
+        </h1>
+        {!!subtext && 
+          <p>
+            {subtext}
+          </p>
+        }
+        {children}
+      </div>
+    </div>
+  )
+}

+ 3 - 0
packages/components/src/components/Header/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import Header from "./Header"
+export default memo(Header)

+ 43 - 0
packages/components/src/components/NavButton/NavButton.style.ts

@@ -0,0 +1,43 @@
+import { css } from "@emotion/core"
+import { typography, colors } from "../../theme"
+
+export type NavButtonStyleProps = {
+  type?: "primary" | "secondary"
+}
+
+export let makeStyles = ({
+  type = "primary"
+}: NavButtonStyleProps) => {
+  return css`
+    border: 0;
+    color: ${colors.white};
+    background-color: ${type === "primary" ? colors.blue[500] : colors.black};
+    text-align: center;
+    display: inline-block;
+    cursor: default;
+    font-family: ${typography.fonts.base};
+    font-weight: ${typography.weights.medium};
+    font-size: ${typography.sizes.subtitle1};
+    margin: 1px;
+    padding: 0;
+    width: 50px;
+    height: 50px;
+    line-height: 50px;
+
+    &:hover {
+      background-color: ${type === "primary" ? colors.blue[700] : colors.black};
+      border-color: ${colors.blue[700]};
+      color: ${type === "primary" ? colors.white : colors.blue[300]};
+    }
+
+    &:active {
+      background-color: ${type === "primary" ? colors.blue[900] : colors.black};
+      border-color: ${colors.blue[900]};
+      color: ${type === "primary" ? colors.white : colors.blue[700]};
+    }
+
+    &::selection {
+      background: transparent;
+    }
+  `
+}

+ 22 - 0
packages/components/src/components/NavButton/NavButton.tsx

@@ -0,0 +1,22 @@
+import React from "react"
+import { makeStyles, NavButtonStyleProps } from "./NavButton.style"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import { faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons"
+
+type NavButtonProps = {
+  direction?: "right" | "left",
+  onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
+} & NavButtonStyleProps
+
+export default function NavButton({
+  direction = "right",
+  onClick,
+  ...styleProps
+}: NavButtonProps) {
+  let styles = makeStyles(styleProps)
+  return (
+    <div css={styles} onClick={onClick}>
+      {direction === "right" ? <FontAwesomeIcon icon={faChevronRight} /> : <FontAwesomeIcon icon={faChevronLeft} />}
+    </div>
+  )
+}

+ 3 - 0
packages/components/src/components/NavButton/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import NavButton from "./NavButton"
+export default memo(NavButton)

+ 24 - 0
packages/components/src/components/Tag/Tag.style.ts

@@ -0,0 +1,24 @@
+import { css } from "@emotion/core"
+import { typography, colors } from "../../theme"
+
+export type TagStyleProps = {}
+
+export let makeStyles = ({}: TagStyleProps) => {
+  return css`
+    border: 1px solid ${colors.blue[700]};
+    color: ${colors.white};
+    background-color: ${colors.black};
+    text-align: center;
+    padding: 10px 15px;
+    display: inline-block;
+    cursor: default;
+    font-family: ${typography.fonts.base};
+    font-weight: ${typography.weights.regular};
+    font-size: ${typography.sizes.button.medium};
+    margin: 0 15px 0 0;
+
+    &::selection {
+      background: transparent;
+    }
+  `
+}

+ 18 - 0
packages/components/src/components/Tag/Tag.tsx

@@ -0,0 +1,18 @@
+import React from "react"
+import { makeStyles, TagStyleProps } from "./Tag.style"
+
+type TagProps = {
+  text: string
+} & TagStyleProps
+
+export default function Tag({
+  text,
+  ...styleProps
+}: TagProps) {
+  let styles = makeStyles(styleProps)
+  return (
+    <div css={styles}>
+      {text}
+    </div>
+  )
+}

+ 3 - 0
packages/components/src/components/Tag/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import Tag from "./Tag"
+export default memo(Tag)

+ 38 - 0
packages/components/src/components/TagButton/TagButton.style.ts

@@ -0,0 +1,38 @@
+import { css } from "@emotion/core"
+import { typography, colors } from "../../theme"
+
+export type TagButtonStyleProps = {
+  selected?: boolean
+}
+
+export let makeStyles = ({
+  selected = false
+}: TagButtonStyleProps) => {
+  return css`
+    border: 1px solid ${colors.blue[500]};
+    color: ${colors.white};
+    background-color: ${colors.black};
+    text-align: center;
+    padding: 15px 20px;
+    display: inline-block;
+    cursor: default;
+    font-family: ${typography.fonts.base};
+    font-weight: ${typography.weights.medium};
+    font-size: ${typography.sizes.button.large};
+    margin: 0 15px 0 0;
+    line-height: ${typography.sizes.button.large};
+    box-shadow: ${selected ? `3px 3px ${colors.blue[500]}` : "none"};
+
+    span {
+      margin-left: 20px;
+      font-size: ${typography.sizes.icon.xxlarge};
+      font-weight: ${typography.weights.regular};
+      line-height: 0;
+      vertical-align: sub;
+    }
+
+    &::selection {
+      background: transparent;
+    }
+  `
+}

+ 20 - 0
packages/components/src/components/TagButton/TagButton.tsx

@@ -0,0 +1,20 @@
+import React from "react"
+import { makeStyles, TagButtonStyleProps } from "./TagButton.style"
+
+type TagButtonProps = {
+  text: string
+  onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
+} & TagButtonStyleProps
+
+export default function TagButton({
+  text,
+  onClick,
+  ...styleProps
+}: TagButtonProps) {
+  let styles = makeStyles(styleProps)
+  return (
+    <div css={styles} onClick={onClick}>
+      {text}<span>+</span>
+    </div>
+  )
+}

+ 3 - 0
packages/components/src/components/TagButton/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import TagButton from "./TagButton"
+export default memo(TagButton)

+ 47 - 0
packages/components/src/components/TextField/TextField.style.ts

@@ -0,0 +1,47 @@
+import { css } from "@emotion/core"
+import { typography, colors } from "./../../theme"
+
+export type TextFieldStyleProps = {
+  disabled?: boolean
+}
+
+export let makeStyles = ({
+  disabled = false
+}: TextFieldStyleProps) => {
+
+  return {
+    container: css`
+      position: relative;
+      min-width: 250px;
+      height: 50px;
+      font-family: ${typography.fonts.base};
+      display: inline-flex;
+    `,
+    border: css`
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      border: 1px solid ${colors.gray[400]};
+      display: flex;
+      align-items: center;
+      justify-content: left;
+    `,
+    label: css`
+      color: ${colors.gray[400]};
+      padding: 0 10px;
+      background-color: ${colors.black};
+      transition: all 0.1s linear;
+    `,
+    input: css`
+      display: none;
+      width: 100%;
+      margin: 0 10px;
+      background: none;
+      border: none;
+      color: ${colors.white};
+      outline: none;
+    `
+  }
+}

+ 84 - 0
packages/components/src/components/TextField/TextField.tsx

@@ -0,0 +1,84 @@
+import React, { useState, useRef, useEffect } from "react"
+import { makeStyles, TextFieldStyleProps } from "./TextField.style"
+import { colors } from "./../../theme"
+import { IconProp } from "@fortawesome/fontawesome-svg-core"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+
+type TextFieldProps = {
+  label: string
+  value?: string,
+  icon?: IconProp
+  iconPosition?: "right" | "left"
+  disabled?: boolean
+  onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
+  onChange?: (e: React.ChangeEvent) => void
+} & TextFieldStyleProps
+
+export default function TextField({
+  label,
+  value = "",
+  icon = null,
+  iconPosition = "right",
+  disabled = false,
+  onClick,
+  onChange,
+  ...styleProps
+}: TextFieldProps) {
+
+  const inputRef = useRef(null)
+  const [isActive, setIsActive] = useState(!!value)
+  const [inputTextValue, setInputTextValue] = useState(value)
+  const styles = makeStyles(styleProps)
+
+  useEffect(() => {
+    if (isActive) {
+      inputRef.current.focus()
+    }
+    else {
+      inputRef.current.blur()
+    }
+  }, [isActive, inputRef]);
+
+  function onTextFieldClick() {
+    setIsActive(true)
+  }
+
+  function onInputTextBlur() {
+    setIsActive(false)
+  }
+
+  function onInputTextChange(event: React.FormEvent<HTMLInputElement>): React.ChangeEventHandler {
+    if (disabled) return
+    setInputTextValue(event.currentTarget.value)
+  }
+  
+  return (
+    <div css={styles.container} onClick={onTextFieldClick}>
+      <div
+        css={styles.border}
+        style={!inputTextValue && !isActive ? {} : {
+          border: `1px solid ${colors.gray[200]}`
+        }}>
+        <div
+          css={styles.label}
+          style={!inputTextValue && !isActive ? {} : {
+            position: "absolute",
+            top: "-8px",
+            left: "5px",
+            fontSize: "0.7rem"
+          }}>
+            {label}
+        </div>
+        <input
+          css={styles.input}
+          style={{ display: !!inputTextValue || isActive ? "block" : "none"}}
+          ref={inputRef}
+          type="text"
+          value={inputTextValue}
+          onChange={disabled ? null : onInputTextChange}
+          onBlur={onInputTextBlur}
+        />
+      </div>
+    </div>
+  )
+}

+ 3 - 0
packages/components/src/components/TextField/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import TextField from "./TextField"
+export default memo(TextField)

+ 113 - 0
packages/components/src/components/Typography/Typography.style.ts

@@ -0,0 +1,113 @@
+import { typography, colors, spacing } from "../../theme"
+
+export type TypographyStyleProps = {
+  variant: "hero" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "subtitle1" | "subtitle2" | "body1" | "body2" | "caption" | "overhead"
+}
+
+export let makeStyles = ({
+  variant = "body1"
+}: TypographyStyleProps) => {
+
+  const base = {
+    fontFamily: typography.fonts.base,
+    color: colors.white
+  }
+
+  let specific = {}
+
+  switch(variant) {
+    case "hero":
+      specific = {
+        fontSize: typography.sizes.hero,
+        fontWeight: typography.weights.medium,
+        margin: `${spacing.xxxl} 0`
+      }
+      break
+    case "h1":
+      specific = {
+        fontSize: typography.sizes.h1,
+        fontWeight: typography.weights.medium,
+        margin: `${spacing.xxl} 0`
+      }
+      break
+    case "h2":
+      specific = {
+        fontSize: typography.sizes.h2,
+        fontWeight: typography.weights.medium,
+        margin: `${spacing.xl} 0`
+      }
+      break
+    case "h3":
+      specific = {
+        fontSize: typography.sizes.h3,
+        fontWeight: typography.weights.medium,
+        margin: `${spacing.l} 0`
+      }
+      break
+    case "h4":
+      specific = {
+        fontSize: typography.sizes.h4,
+        fontWeight: typography.weights.medium,
+        margin: `${spacing.l} 0`
+      }
+      break
+    case "h5":
+      specific = {
+        fontSize: typography.sizes.h5,
+        fontWeight: typography.weights.medium,
+        margin: `${spacing.m} 0`
+      }
+      break
+    case "h6":
+      specific = {
+        fontSize: typography.sizes.h6,
+        fontWeight: typography.weights.medium,
+        margin: `${spacing.m} 0`
+      }
+      break
+    case "subtitle1":
+      specific = {
+        fontSize: typography.sizes.subtitle1,
+        fontWeight: typography.weights.light,
+        margin: `${spacing.l} 0`
+      }
+      break
+    case "subtitle2":
+      specific = {
+        fontSize: typography.sizes.subtitle2,
+        fontWeight: typography.weights.regular,
+        margin: `${spacing.m} 0`
+      }
+      break
+    case "body1":
+      specific = {
+        fontSize: typography.sizes.body1,
+        fontWeight: typography.weights.light,
+        margin: `${spacing.s} 0`
+      }
+      break
+    case "body2":
+      specific = {
+        fontSize: typography.sizes.body2,
+        fontWeight: typography.weights.light,
+        margin: `${spacing.xs} 0`
+      }
+      break
+    case "caption":
+      specific = {
+        fontSize: typography.sizes.caption,
+        fontWeight: typography.weights.light,
+        margin: `${spacing.xs} 0`
+      }
+      break
+    case "overhead":
+      specific = {
+        fontSize: typography.sizes.overhead,
+        fontWeight: typography.weights.regular,
+        margin: `${spacing.xs} 0`
+      }
+    break
+  }
+
+  return { ...base, ...specific }
+}

+ 20 - 0
packages/components/src/components/Typography/Typography.tsx

@@ -0,0 +1,20 @@
+import React from "react"
+import { makeStyles, TypographyStyleProps } from "./Typography.style"
+
+type TypographyProps = {
+  children: React.ReactNode
+  onClick?: (e: React.MouseEvent<any>) => void
+} & TypographyStyleProps
+
+export default function Typography({
+  children,
+  onClick,
+  ...styleProps
+}: TypographyProps) {
+  let styles = makeStyles(styleProps)
+  return (
+    <div css={styles} onClick={onClick}>
+      {children}
+    </div>
+  )
+}

+ 3 - 0
packages/components/src/components/Typography/index.ts

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import Typography from "./Typography"
+export default memo(Typography)

+ 33 - 0
packages/components/src/components/VideoPlayer/VideoPlayer.style.tsx

@@ -0,0 +1,33 @@
+import { css } from "@emotion/core";
+import { breakpoints } from "../../theme";
+
+export type VideoPlayerStyleProps = {
+  width?: string | number;
+  height?: string | number;
+  responsive?: boolean;
+  ratio?: string;
+};
+
+export let makeStyles = ({
+  width = "100%",
+  height = "100%",
+  responsive = true,
+  ratio = "16:9",
+}: VideoPlayerStyleProps) => {
+  let ratioPerc = ratio
+    .split(":")
+    .map(x => Number(x))
+    .reduce((x, y) => (y / x) * 100);
+
+  return {
+    containerStyles: css`
+      max-width: ${breakpoints.medium};
+      & .video-player {
+      }
+    `,
+    playerStyles: css`
+      width: ${width};
+      height: ${height};
+    `,
+  };
+};

+ 79 - 0
packages/components/src/components/VideoPlayer/VideoPlayer.tsx

@@ -0,0 +1,79 @@
+import React from "react";
+import ReactPlayer from "react-player";
+import { VideoPlayerStyleProps, makeStyles } from "./VideoPlayer.style";
+
+export type VideoPlayerProps = {
+  src?: string;
+  playing?: boolean;
+  poster?: string;
+  controls?: boolean;
+  volume?: number;
+  loop?: boolean;
+  autoPlay?: boolean;
+  muted?: boolean;
+  className?: string;
+  onReady?(): void;
+  onStart?(): void;
+  onPlay?(): void;
+  onPause?(): void;
+  onBuffer?(): void;
+  onEnded?(): void;
+  onError?(error: any): void;
+  onDuration?(duration: number): void;
+  onProgress?(state: { played: number; loaded: number }): void;
+} & VideoPlayerStyleProps;
+
+export default function VideoPlayer({
+  src,
+  poster,
+  playing,
+  onPause,
+  autoPlay,
+  loop = false,
+  muted = true,
+  onStart,
+  ratio,
+  onReady,
+  onPlay,
+  onBuffer,
+  onError,
+  onEnded,
+  onDuration,
+  onProgress,
+  className,
+  volume = 0.7,
+  controls = true,
+  ...styleProps
+}: VideoPlayerProps) {
+  let { playerStyles, containerStyles } = makeStyles(styleProps);
+  return (
+    <div css={containerStyles}>
+      <ReactPlayer
+        css={playerStyles}
+        width={styleProps.responsive ? "100%" : styleProps.width}
+        height={styleProps.responsive ? "100%" : styleProps.height}
+        url={src}
+        autoPlay={autoPlay}
+        light={poster || true}
+        className={className}
+        playing={playing}
+        loop={loop}
+        controls={controls}
+        onStart={onStart}
+        onPlay={onPlay}
+        onBuffer={onBuffer}
+        onReady={onReady}
+        onEnded={onEnded}
+        onDuration={onDuration}
+        onProgress={onProgress}
+        config={{
+          file: {
+            attributes: {
+              className: "video-player",
+            },
+          },
+        }}
+      />
+    </div>
+  );
+}

+ 3 - 0
packages/components/src/components/VideoPlayer/index.tsx

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import VideoPlayer from "./VideoPlayer"
+export default memo(VideoPlayer)

+ 47 - 0
packages/components/src/components/VideoPreview/VideoPreview.styles.tsx

@@ -0,0 +1,47 @@
+import { css } from "@emotion/core"
+import { typography, colors } from "./../../theme"
+
+export type VideoPreviewStyleProps = {
+  showChannel?: boolean
+}
+
+export let makeStyles = ({ showChannel = false }: VideoPreviewStyleProps) => {
+  return {
+    container: css``,
+    link: css`
+      text-decoration: none;
+    `,
+    coverContainer: css`
+      width: 100%;
+      background-color: black;
+    `,
+    cover: css`
+      display: block;
+      width: 100%;
+      height: auto;
+    `,
+    infoContainer: css`
+      display: grid;
+      grid-template: auto / ${showChannel ? "45px auto" : "auto"};
+      margin: 10px 0 0;
+    `,
+    avatar: css`
+      grid-column: 1 / 1;
+    `,
+    textContainer: css`
+      grid-column: ${showChannel ? "2 / 2" : "1 / 1"};
+    `,
+    title: css`
+      margin: 0;
+      font-weight: ${typography.weights.bold};
+      text-transform: capitalize;
+      color: ${colors.black.base};
+      font-size: ${typography.sizes.small};
+    `,
+    channel: css`
+      margin: 5px 0 0;
+      font-size: ${typography.sizes.xsmall};
+      color: ${colors.grey.darker};
+    `
+  }
+}

+ 47 - 0
packages/components/src/components/VideoPreview/VideoPreview.tsx

@@ -0,0 +1,47 @@
+import React from "react"
+
+import { makeStyles, VideoPreviewStyleProps } from "./VideoPreview.styles"
+import Avatar from "./../Avatar"
+
+type VideoPreviewProps = {
+  title: string
+  channel?: string
+  channelImg?: string
+  showChannel?: boolean
+  poster?: string
+  onClick?: any
+  onChannelClick?: any
+} & VideoPreviewStyleProps
+
+export default function VideoPreview({
+  title,
+  channel,
+  channelImg,
+  showChannel = false,
+  poster,
+  onClick,
+  onChannelClick,
+  ...styleProps
+}: VideoPreviewProps) {
+  let styles = makeStyles({ showChannel, ...styleProps })
+  return (
+    <div css={styles.container} onClick={onClick}>
+      <div css={styles.coverContainer}>
+        <img css={styles.cover} src={poster} onClick={event => { event.stopPropagation(); onClick() }} />
+      </div>
+      <div css={styles.infoContainer}>
+        {showChannel && (
+          <div css={styles.avatar} onClick={event => { event.stopPropagation(); onChannelClick() }}>
+            <Avatar size="small" img={channelImg} />
+          </div>
+        )}
+        <div css={styles.textContainer}>
+          <h3 css={styles.title} onClick={event => { event.stopPropagation(); onClick() }}>{title}</h3>
+          {showChannel && (
+            <h3 css={styles.channel} onClick={event => { event.stopPropagation(); onChannelClick() }}>{channel}</h3>
+          )}
+        </div>
+      </div>
+    </div>
+  )
+}

+ 3 - 0
packages/components/src/components/VideoPreview/index.tsx

@@ -0,0 +1,3 @@
+import { memo } from "react"
+import VideoPreview from "./VideoPreview"
+export default memo(VideoPreview)

+ 8 - 0
packages/components/src/index.ts

@@ -0,0 +1,8 @@
+export { default as Button } from "./components/Button"
+export { default as NavButton } from "./components/NavButton"
+export { default as TagButton } from "./components/TagButton"
+export { default as Header } from "./components/Header"
+export { default as Tag } from "./components/Tag"
+export { default as TextField } from "./components/TextField"
+export { default as Typography } from "./components/Typography"
+export { default as Grid } from "./components/Grid"

+ 5 - 0
packages/components/src/theme/breakpoints.ts

@@ -0,0 +1,5 @@
+export default {
+  small: "21rem",
+  medium: "48rem",
+  large: "72rem",
+};

Some files were not shown because too many files changed in this diff