Browse Source

add graphql/miragejs data fetching

Klaudiusz Dembler 4 years ago
parent
commit
e82cf71f27
38 changed files with 1538 additions and 237 deletions
  1. 12 0
      packages/app/__generated__/globalTypes.ts
  2. 9 0
      packages/app/apollo.config.js
  3. 6 1
      packages/app/config-overrides.js
  4. 10 3
      packages/app/package.json
  5. 10 6
      packages/app/src/App.tsx
  6. 22 0
      packages/app/src/api/client.ts
  7. 3 0
      packages/app/src/api/index.ts
  8. 16 0
      packages/app/src/api/queries/__generated__/ChannelFields.ts
  9. 43 0
      packages/app/src/api/queries/__generated__/GetFeaturedVideos.ts
  10. 20 0
      packages/app/src/api/queries/__generated__/GetNewestChannels.ts
  11. 43 0
      packages/app/src/api/queries/__generated__/GetNewestVideos.ts
  12. 39 0
      packages/app/src/api/queries/__generated__/VideoFields.ts
  13. 20 0
      packages/app/src/api/queries/channels.ts
  14. 2 0
      packages/app/src/api/queries/index.ts
  15. 45 0
      packages/app/src/api/queries/videos.ts
  16. 24 16
      packages/app/src/components/ChannelGallery.tsx
  17. 12 9
      packages/app/src/components/VideoGallery.tsx
  18. 0 65
      packages/app/src/config/mocks/mockChannels.ts
  19. 0 0
      packages/app/src/mocking/data/index.ts
  20. 67 0
      packages/app/src/mocking/data/mockChannels.ts
  21. 0 0
      packages/app/src/mocking/data/mockImages.ts
  22. 24 32
      packages/app/src/mocking/data/mockVideos.ts
  23. 62 0
      packages/app/src/mocking/server.ts
  24. 202 0
      packages/app/src/schema.graphql
  25. 2 2
      packages/app/src/shared/components/Avatar/Avatar.style.tsx
  26. 4 11
      packages/app/src/shared/components/Avatar/Avatar.tsx
  27. 5 6
      packages/app/src/shared/components/VideoPreview/VideoPreview.tsx
  28. 0 8
      packages/app/src/types/channel.ts
  29. 6 0
      packages/app/src/types/graphql.d.ts
  30. 1 0
      packages/app/src/types/graphqlScalars.d.ts
  31. 1 0
      packages/app/src/types/mirage.d.ts
  32. 0 17
      packages/app/src/types/video.ts
  33. 0 7
      packages/app/src/types/videoMedia.ts
  34. 0 5
      packages/app/src/utils/date.ts
  35. 24 6
      packages/app/src/utils/time.ts
  36. 38 18
      packages/app/src/views/HomeView.tsx
  37. 20 10
      packages/app/src/views/VideoView/VideoView.tsx
  38. 746 15
      yarn.lock

+ 12 - 0
packages/app/__generated__/globalTypes.ts

@@ -0,0 +1,12 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+//==============================================================
+// START Enums and Input Objects
+//==============================================================
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================

+ 9 - 0
packages/app/apollo.config.js

@@ -0,0 +1,9 @@
+module.exports = {
+  client: {
+    service: {
+      name: 'atlas-graphql',
+      localSchemaFile: 'src/schema.graphql',
+    },
+    excludes: ['src/schema.graphql'],
+  },
+}

+ 6 - 1
packages/app/config-overrides.js

@@ -1,6 +1,6 @@
 /* eslint-disable @typescript-eslint/no-var-requires */
 const path = require('path')
-const { override, addBabelPreset, addWebpackAlias, disableEsLint } = require('customize-cra')
+const { override, addBabelPreset, addWebpackAlias, disableEsLint, addWebpackModuleRule } = require('customize-cra')
 const eslintConfig = require('../../.eslintrc.js')
 
 const modifiedEslintConfig = {
@@ -50,6 +50,11 @@ module.exports = {
     addWebpackAlias({
       '@': path.resolve(__dirname, 'src/'),
     }),
+    addWebpackModuleRule({
+      test: /\.(graphql|gql)$/,
+      exclude: /node_modules/,
+      loader: 'graphql-tag/loader',
+    }),
     customEslintConfig(modifiedEslintConfig)
   ),
   paths: (paths) => {

+ 10 - 3
packages/app/package.json

@@ -26,11 +26,15 @@
     "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
     "storybook": "start-storybook -p 6006 --quiet -c src/shared/.storybook",
     "build-storybook": "build-storybook -c src/shared/.storybook",
-    "chromatic": "chromatic --project-token=qq8aetz26u"
+    "chromatic": "chromatic --project-token=qq8aetz26u",
+    "queries:codegen": "apollo client:codegen --target typescript --passthroughCustomScalars --customScalarsPrefix=GQL",
+    "queries:watch": "yarn queries:codegen --watch"
   },
   "dependencies": {
+    "@apollo/client": "^3.1.1",
     "@emotion/babel-preset-css-prop": "^10.0.27",
     "@emotion/core": "^10.0.28",
+    "@miragejs/graphql": "^0.1.0",
     "@reach/router": "^1.3.3",
     "@storybook/addon-actions": "^5.3.17",
     "@storybook/addon-docs": "^5.3.17",
@@ -42,18 +46,21 @@
     "@storybook/react": "^5.3.17",
     "@storybook/theming": "^5.3.19",
     "@types/lodash": "^4.14.157",
-    "@types/luxon": "^1.24.1",
     "@types/reach__router": "^1.3.5",
     "@types/react": "^16.9.0",
     "@types/react-dom": "^16.9.0",
     "@types/react-redux": "^7.1.9",
     "@types/redux": "^3.6.0",
     "@types/video.js": "^7.3.10",
+    "apollo": "^2.30.2",
     "chromatic": "^4.0.3",
     "customize-cra": "^1.0.0",
+    "date-fns": "^2.15.0",
     "emotion-normalize": "^10.1.0",
+    "graphql": "^15.3.0",
+    "graphql-tag": "^2.11.0",
     "lodash": "^4.17.19",
-    "luxon": "^1.24.1",
+    "miragejs": "^0.1.40",
     "react": "^16.13.1",
     "react-app-rewired": "^2.1.6",
     "react-docgen-typescript-loader": "^3.7.1",

+ 10 - 6
packages/app/src/App.tsx

@@ -1,21 +1,25 @@
 import React from 'react'
 import { Provider } from 'react-redux'
 import { Router } from '@reach/router'
+import { ApolloProvider } from '@apollo/client'
 
 import store from './store'
 import { Layout } from './components'
 import { HomeView, VideoView } from './views'
 import routes from './config/routes'
+import { apolloClient } from '@/api'
 
 export default function App() {
   return (
     <Provider store={store}>
-      <Layout>
-        <Router primary={false}>
-          <HomeView default />
-          <VideoView path={routes.video} />
-        </Router>
-      </Layout>
+      <ApolloProvider client={apolloClient}>
+        <Layout>
+          <Router primary={false}>
+            <HomeView default />
+            <VideoView path={routes.video} />
+          </Router>
+        </Layout>
+      </ApolloProvider>
     </Provider>
   )
 }

+ 22 - 0
packages/app/src/api/client.ts

@@ -0,0 +1,22 @@
+import { ApolloClient, InMemoryCache } from '@apollo/client'
+import { parseISO } from 'date-fns'
+import '@/mocking/server'
+
+const apolloClient = new ApolloClient({
+  uri: '/graphql',
+  cache: new InMemoryCache({
+    typePolicies: {
+      Video: {
+        fields: {
+          publishedOnJoystreamAt: {
+            merge(_, publishedOnJoystreamAt: string): Date {
+              return parseISO(publishedOnJoystreamAt)
+            },
+          },
+        },
+      },
+    },
+  }),
+})
+
+export default apolloClient

+ 3 - 0
packages/app/src/api/index.ts

@@ -0,0 +1,3 @@
+import apolloClient from '@/api/client'
+
+export { apolloClient }

+ 16 - 0
packages/app/src/api/queries/__generated__/ChannelFields.ts

@@ -0,0 +1,16 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: ChannelFields
+// ====================================================
+
+export interface ChannelFields {
+  __typename: 'Channel'
+  id: string
+  handle: string
+  avatarPhotoURL: string
+  totalViews: number
+}

+ 43 - 0
packages/app/src/api/queries/__generated__/GetFeaturedVideos.ts

@@ -0,0 +1,43 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetFeaturedVideos
+// ====================================================
+
+export interface GetFeaturedVideos_featured_videos_media_location {
+  __typename: 'HTTPVideoMediaLocation'
+  host: string
+  port: number | null
+}
+
+export interface GetFeaturedVideos_featured_videos_media {
+  __typename: 'VideoMedia'
+  location: GetFeaturedVideos_featured_videos_media_location
+}
+
+export interface GetFeaturedVideos_featured_videos_channel {
+  __typename: 'Channel'
+  id: string
+  avatarPhotoURL: string
+  handle: string
+}
+
+export interface GetFeaturedVideos_featured_videos {
+  __typename: 'Video'
+  id: string
+  title: string
+  description: string
+  views: number
+  duration: number
+  thumbnailURL: string
+  publishedOnJoystreamAt: GQLDate
+  media: GetFeaturedVideos_featured_videos_media
+  channel: GetFeaturedVideos_featured_videos_channel
+}
+
+export interface GetFeaturedVideos {
+  featured_videos: GetFeaturedVideos_featured_videos[]
+}

+ 20 - 0
packages/app/src/api/queries/__generated__/GetNewestChannels.ts

@@ -0,0 +1,20 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetNewestChannels
+// ====================================================
+
+export interface GetNewestChannels_channels {
+  __typename: 'Channel'
+  id: string
+  handle: string
+  avatarPhotoURL: string
+  totalViews: number
+}
+
+export interface GetNewestChannels {
+  channels: GetNewestChannels_channels[]
+}

+ 43 - 0
packages/app/src/api/queries/__generated__/GetNewestVideos.ts

@@ -0,0 +1,43 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetNewestVideos
+// ====================================================
+
+export interface GetNewestVideos_videos_media_location {
+  __typename: 'HTTPVideoMediaLocation'
+  host: string
+  port: number | null
+}
+
+export interface GetNewestVideos_videos_media {
+  __typename: 'VideoMedia'
+  location: GetNewestVideos_videos_media_location
+}
+
+export interface GetNewestVideos_videos_channel {
+  __typename: 'Channel'
+  id: string
+  avatarPhotoURL: string
+  handle: string
+}
+
+export interface GetNewestVideos_videos {
+  __typename: 'Video'
+  id: string
+  title: string
+  description: string
+  views: number
+  duration: number
+  thumbnailURL: string
+  publishedOnJoystreamAt: GQLDate
+  media: GetNewestVideos_videos_media
+  channel: GetNewestVideos_videos_channel
+}
+
+export interface GetNewestVideos {
+  videos: GetNewestVideos_videos[]
+}

+ 39 - 0
packages/app/src/api/queries/__generated__/VideoFields.ts

@@ -0,0 +1,39 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: VideoFields
+// ====================================================
+
+export interface VideoFields_media_location {
+  __typename: 'HTTPVideoMediaLocation'
+  host: string
+  port: number | null
+}
+
+export interface VideoFields_media {
+  __typename: 'VideoMedia'
+  location: VideoFields_media_location
+}
+
+export interface VideoFields_channel {
+  __typename: 'Channel'
+  id: string
+  avatarPhotoURL: string
+  handle: string
+}
+
+export interface VideoFields {
+  __typename: 'Video'
+  id: string
+  title: string
+  description: string
+  views: number
+  duration: number
+  thumbnailURL: string
+  publishedOnJoystreamAt: GQLDate
+  media: VideoFields_media
+  channel: VideoFields_channel
+}

+ 20 - 0
packages/app/src/api/queries/channels.ts

@@ -0,0 +1,20 @@
+import gql from 'graphql-tag'
+
+const channelFieldsFragment = gql`
+  fragment ChannelFields on Channel {
+    id
+    handle
+    avatarPhotoURL
+    totalViews
+  }
+`
+
+// TODO: Add proper query params (order, limit, etc.)
+export const GET_NEWEST_CHANNELS = gql`
+  query GetNewestChannels {
+    channels {
+      ...ChannelFields
+    }
+  }
+  ${channelFieldsFragment}
+`

+ 2 - 0
packages/app/src/api/queries/index.ts

@@ -0,0 +1,2 @@
+export * from './videos'
+export * from './channels'

+ 45 - 0
packages/app/src/api/queries/videos.ts

@@ -0,0 +1,45 @@
+import gql from 'graphql-tag'
+
+const videoFieldsFragment = gql`
+  fragment VideoFields on Video {
+    id
+    title
+    description
+    views
+    duration
+    thumbnailURL
+    publishedOnJoystreamAt
+    media {
+      location {
+        ... on HTTPVideoMediaLocation {
+          host
+          port
+        }
+      }
+    }
+    channel {
+      id
+      avatarPhotoURL
+      handle
+    }
+  }
+`
+
+// TODO: Add proper query params (order, limit, etc.)
+export const GET_NEWEST_VIDEOS = gql`
+  query GetNewestVideos {
+    videos {
+      ...VideoFields
+    }
+  }
+  ${videoFieldsFragment}
+`
+
+export const GET_FEATURED_VIDEOS = gql`
+  query GetFeaturedVideos {
+    featured_videos {
+      ...VideoFields
+    }
+  }
+  ${videoFieldsFragment}
+`

+ 24 - 16
packages/app/src/components/ChannelGallery.tsx

@@ -1,27 +1,35 @@
 import React from 'react'
 import { css } from '@emotion/core'
 import { ChannelPreview, Gallery } from '@/shared/components'
-import { mockChannels } from '@/config/mocks'
+import { ChannelFields } from '@/api/queries/__generated__/ChannelFields'
 
 type ChannelGalleryProps = {
   title: string
   action?: string
+  channels?: ChannelFields[]
+  loading?: boolean
 }
 
-const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, action }) => (
-  <Gallery title={title} action={action}>
-    {mockChannels.map((channel) => (
-      <ChannelPreview
-        name={channel.name}
-        avatarURL={channel.avatarURL}
-        views={channel.views}
-        key={channel.id}
-        outerContainerCss={css`
-          margin-right: 1.5rem;
-        `}
-      />
-    ))}
-  </Gallery>
-)
+const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, action, channels, loading }) => {
+  if (loading || !channels) {
+    return <p>Loading</p>
+  }
+
+  return (
+    <Gallery title={title} action={action}>
+      {channels.map((channel) => (
+        <ChannelPreview
+          name={channel.handle}
+          avatarURL={channel.avatarPhotoURL}
+          views={channel.totalViews}
+          key={channel.id}
+          outerContainerCss={css`
+            margin-right: 1.5rem;
+          `}
+        />
+      ))}
+    </Gallery>
+  )
+}
 
 export default ChannelGallery

+ 12 - 9
packages/app/src/components/VideoGallery.tsx

@@ -4,12 +4,13 @@ import { navigate } from '@reach/router'
 
 import { Gallery, VideoPreview } from '@/shared/components'
 import theme from '@/shared/theme'
-import { mockVideos } from '@/config/mocks'
-import { shuffle } from 'lodash'
+import { VideoFields } from '@/api/queries/__generated__/VideoFields'
 
 type VideoGalleryProps = {
   title: string
-  action: string
+  action?: string
+  videos?: VideoFields[]
+  loading?: boolean
 }
 
 const articleStyles = css`
@@ -19,7 +20,7 @@ const articleStyles = css`
 
 const CAROUSEL_WHEEL_HEIGHT = theme.sizes.b12
 
-const VideoGallery: React.FC<Partial<VideoGalleryProps>> = ({ title, action }) => {
+const VideoGallery: React.FC<VideoGalleryProps> = ({ title, action, videos, loading }) => {
   const [posterSize, setPosterSize] = useState(0)
   const [galleryControlCss, setGalleryControlCss] = useState<SerializedStyles>(css``)
 
@@ -44,7 +45,9 @@ const VideoGallery: React.FC<Partial<VideoGalleryProps>> = ({ title, action }) =
     navigate('/video/fake')
   }
 
-  const videos = shuffle(mockVideos)
+  if (loading || !videos) {
+    return <p>Loading</p>
+  }
 
   return (
     <Gallery title={title} action={action} leftControlCss={galleryControlCss} rightControlCss={galleryControlCss}>
@@ -52,12 +55,12 @@ const VideoGallery: React.FC<Partial<VideoGalleryProps>> = ({ title, action }) =
         <article css={articleStyles} key={`${title}- ${video.title} - ${idx}`}>
           <VideoPreview
             title={video.title}
-            channelName={video.channel.name}
-            channelAvatarURL={video.channel.avatarURL}
+            channelName={video.channel.handle}
+            channelAvatarURL={video.channel.avatarPhotoURL}
             views={video.views}
-            createdAt={video.createdAt}
+            createdAt={video.publishedOnJoystreamAt}
             duration={video.duration}
-            posterURL={video.posterURL}
+            posterURL={video.thumbnailURL}
             onClick={handleVideoClick}
             imgRef={idx === 0 ? imgRef : null}
           />

+ 0 - 65
packages/app/src/config/mocks/mockChannels.ts

@@ -1,65 +0,0 @@
-import { shuffle } from 'lodash'
-import { channelSources } from '@/config/mocks/mockImages'
-import Channel from '@/types/channel'
-
-const rawChannels = [
-  {
-    id: 'acc5d2e6-9769-410f-a75f-dca4e3311ef5',
-    name: 'eleifend',
-    views: 3743422,
-  },
-  {
-    id: '8ec449d7-ea83-4733-8844-24f4ec6df796',
-    name: 'ipsum primis in faucibus',
-    views: 125,
-  },
-  {
-    id: 'b0f895d7-06eb-4f0a-83a6-914ac89a1e3b',
-    name: 'augue luctus tincidunt',
-    views: 3189965,
-  },
-  {
-    id: '5c9237ae-34af-4c6a-b175-7e0e2e632d0b',
-    name: 'vel',
-    views: 3759,
-  },
-  {
-    id: 'e7d5f6c0-e4a5-43dc-a950-160418867ad1',
-    name: 'elementum ligula',
-    views: 16,
-  },
-  {
-    id: '7df156ea-b7be-4911-b721-e25be826011f',
-    name: 'phasellus sit amet erat nulla',
-    views: 4344626,
-  },
-  {
-    id: 'c81d9e66-fc1a-4c3c-95b4-a2589e19d430',
-    name: 'amet sapien dignissim',
-    views: 239128,
-  },
-  {
-    id: 'ca51ed2b-0ae7-4806-bc12-327326c47ff3',
-    name: 'maecenas rhoncus aliquam',
-    views: 1300671,
-  },
-  {
-    id: '015f411a-5ebd-40a3-aee5-9fab3b433bb0',
-    name: 'et commodo vulputate justo',
-    views: 4834863,
-  },
-  {
-    id: 'e9bc9b46-04c6-49a4-ab26-445a271f5a06',
-    name: 'in',
-    views: 1509,
-  },
-]
-
-const shuffledRawChannels = shuffle(rawChannels)
-const unshuffledChannels: Channel[] = shuffledRawChannels.map((rawChannel, idx) => ({
-  ...rawChannel,
-  avatarURL: channelSources[idx % channelSources.length],
-}))
-const mockChannels = shuffle(unshuffledChannels)
-
-export default mockChannels

+ 0 - 0
packages/app/src/config/mocks/index.ts → packages/app/src/mocking/data/index.ts


+ 67 - 0
packages/app/src/mocking/data/mockChannels.ts

@@ -0,0 +1,67 @@
+import { shuffle } from 'lodash'
+import { channelSources } from './mockImages'
+import { ChannelFields } from '@/api/queries/__generated__/ChannelFields'
+
+const rawChannels = [
+  {
+    id: 'acc5d2e6-9769-410f-a75f-dca4e3311ef5',
+    handle: 'eleifend',
+    totalViews: 3743422,
+  },
+  {
+    id: '8ec449d7-ea83-4733-8844-24f4ec6df796',
+    handle: 'ipsum primis in faucibus',
+    totalViews: 125,
+  },
+  {
+    id: 'b0f895d7-06eb-4f0a-83a6-914ac89a1e3b',
+    handle: 'augue luctus tincidunt',
+    totalViews: 3189965,
+  },
+  {
+    id: '5c9237ae-34af-4c6a-b175-7e0e2e632d0b',
+    handle: 'vel',
+    totalViews: 3759,
+  },
+  {
+    id: 'e7d5f6c0-e4a5-43dc-a950-160418867ad1',
+    handle: 'elementum ligula',
+    totalViews: 16,
+  },
+  {
+    id: '7df156ea-b7be-4911-b721-e25be826011f',
+    handle: 'phasellus sit amet erat nulla',
+    totalViews: 4344626,
+  },
+  {
+    id: 'c81d9e66-fc1a-4c3c-95b4-a2589e19d430',
+    handle: 'amet sapien dignissim',
+    totalViews: 239128,
+  },
+  {
+    id: 'ca51ed2b-0ae7-4806-bc12-327326c47ff3',
+    handle: 'maecenas rhoncus aliquam',
+    totalViews: 1300671,
+  },
+  {
+    id: '015f411a-5ebd-40a3-aee5-9fab3b433bb0',
+    handle: 'et commodo vulputate justo',
+    totalViews: 4834863,
+  },
+  {
+    id: 'e9bc9b46-04c6-49a4-ab26-445a271f5a06',
+    handle: 'in',
+    totalViews: 1509,
+  },
+]
+
+type RawChannel = ChannelFields
+
+const shuffledRawChannels = shuffle(rawChannels)
+const mockChannels: RawChannel[] = shuffledRawChannels.map((rawChannel, idx) => ({
+  ...rawChannel,
+  __typename: 'Channel',
+  avatarPhotoURL: channelSources[idx % channelSources.length],
+}))
+
+export default mockChannels

+ 0 - 0
packages/app/src/config/mocks/mockImages.ts → packages/app/src/mocking/data/mockImages.ts


+ 24 - 32
packages/app/src/config/mocks/mockVideos.ts → packages/app/src/mocking/data/mockVideos.ts

@@ -1,8 +1,6 @@
-import Video from '@/types/video'
-import { DateTime, Duration } from 'luxon'
 import { shuffle } from 'lodash'
 import { posterSources } from './mockImages'
-import mockedChannels from '@/config/mocks/mockChannels'
+import { VideoFields } from '@/api/queries/__generated__/VideoFields'
 
 const rawVideos = [
   {
@@ -11,7 +9,7 @@ const rawVideos = [
     description:
       'Nam dui. Proin leo odio, porttitor id, consequat in, consequat ut, nulla. Sed accumsan felis. Ut at dolor quis odio consequat varius. Integer ac leo. Pellentesque ultrices mattis odio. Donec vitae nisi. Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.',
     views: 888346,
-    createdAt: '2017-08-25T13:40:41Z',
+    publishedOnJoystreamAt: '2017-08-25T13:40:41Z',
     duration: 487,
   },
   {
@@ -20,8 +18,8 @@ const rawVideos = [
     description:
       'Integer ac leo. Pellentesque ultrices mattis odio. Donec vitae nisi. Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla. Sed vel enim sit amet nunc viverra dapibus. Nulla suscipit ligula in lacus. Curabitur at ipsum ac tellus semper interdum. Mauris ullamcorper purus sit amet nulla. Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam. Nam tristique tortor eu pede.',
     views: 1057608,
-    createdAt: '2018-12-04T12:56:11Z',
-    duration: 717,
+    publishedOnJoystreamAt: '2018-12-04T12:56:11Z',
+    duration: 71,
   },
   {
     id: 'cd214dd2-6f29-4473-a60a-c665c0eb9df0',
@@ -29,7 +27,7 @@ const rawVideos = [
     description:
       'Donec semper sapien a libero. Nam dui. Proin leo odio, porttitor id, consequat in, consequat ut, nulla. Sed accumsan felis.',
     views: 960251,
-    createdAt: '2013-07-10T13:01:00Z',
+    publishedOnJoystreamAt: '2013-07-10T13:01:00Z',
     duration: 442,
   },
   {
@@ -38,8 +36,8 @@ const rawVideos = [
     description:
       'Nulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi. Cras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque. Quisque porta volutpat erat. Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla. Nunc purus. Phasellus in felis.',
     views: 529466,
-    createdAt: '2019-09-04T14:56:26Z',
-    duration: 3158,
+    publishedOnJoystreamAt: '2019-09-04T14:56:26Z',
+    duration: 315,
   },
   {
     id: '502c026c-a9c4-4042-a6b1-6391e7bb1908',
@@ -47,7 +45,7 @@ const rawVideos = [
     description:
       'Proin at turpis a pede posuere nonummy. Integer non velit. Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi. Integer ac neque. Duis bibendum. Morbi non quam nec dui luctus rutrum. Nulla tellus.',
     views: 1021837,
-    createdAt: '2011-11-07T12:32:01Z',
+    publishedOnJoystreamAt: '2011-11-07T12:32:01Z',
     duration: 3606,
   },
   {
@@ -56,8 +54,8 @@ const rawVideos = [
     description:
       'Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh. Quisque id justo sit amet sapien dignissim vestibulum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est. Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros. Vestibulum ac est lacinia nisi venenatis tristique. Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue. Aliquam erat volutpat.',
     views: 196110,
-    createdAt: '2016-04-30T02:36:12Z',
-    duration: 1341,
+    publishedOnJoystreamAt: '2016-04-30T02:36:12Z',
+    duration: 134,
   },
   {
     id: 'd3ecc339-369d-4c74-b914-3a695ba5a68c',
@@ -65,7 +63,7 @@ const rawVideos = [
     description:
       'Vivamus in felis eu sapien cursus vestibulum. Proin eu mi. Nulla ac enim. In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem. Duis aliquam convallis nunc. Proin at turpis a pede posuere nonummy. Integer non velit. Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.',
     views: 95954,
-    createdAt: '2011-11-23T02:46:49Z',
+    publishedOnJoystreamAt: '2011-11-23T02:46:49Z',
     duration: 716,
   },
   {
@@ -74,8 +72,8 @@ const rawVideos = [
     description:
       'In hac habitasse platea dictumst. Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem. Integer tincidunt ante vel ipsum. Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo placerat. Praesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.',
     views: 870023,
-    createdAt: '2020-01-07T13:17:49Z',
-    duration: 16,
+    publishedOnJoystreamAt: '2020-01-07T13:17:49Z',
+    duration: 1,
   },
   {
     id: 'c557b154-ee2a-4548-a0c5-4a74774ecf58',
@@ -83,7 +81,7 @@ const rawVideos = [
     description:
       'Mauris lacinia sapien quis libero. Nullam sit amet turpis elementum ligula vehicula consequat. Morbi a ipsum. Integer a nibh.',
     views: 429603,
-    createdAt: '2018-05-16T15:55:50Z',
+    publishedOnJoystreamAt: '2018-05-16T15:55:50Z',
     duration: 1181,
   },
   {
@@ -92,29 +90,23 @@ const rawVideos = [
     description:
       'Sed sagittis. Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci. Nullam molestie nibh in lectus. Pellentesque at nulla. Suspendisse potenti. Cras in purus eu magna vulputate luctus.',
     views: 258527,
-    createdAt: '2010-10-27T09:34:26Z',
-    duration: 3070,
+    publishedOnJoystreamAt: '2020-08-10T12:32:00Z',
+    duration: 307,
   },
 ]
 
-const shuffledRawVideos = shuffle(rawVideos)
-const unshuffledVideos: Video[] = shuffledRawVideos.map((rawVideo, idx) => {
-  const createdAt = DateTime.fromISO(rawVideo.createdAt)
-  const duration = Duration.fromMillis(rawVideo.duration * 1000)
+type RawVideo = Omit<VideoFields, 'media' | 'channel' | 'publishedOnJoystreamAt' | 'duration'> & {
+  publishedOnJoystreamAt: unknown
+  duration: unknown
+}
 
+const shuffledRawVideos = shuffle(rawVideos)
+const mockVideos: RawVideo[] = shuffledRawVideos.map((rawVideo, idx) => {
   return {
     ...rawVideo,
-    createdAt,
-    duration,
-    posterURL: posterSources[idx % posterSources.length],
-    media: {
-      pixelWidth: 1920,
-      pixelHeight: 1080,
-      location: 'https://js-video-example.s3.eu-central-1.amazonaws.com/waves.mp4',
-    },
-    channel: mockedChannels[idx % mockedChannels.length],
+    __typename: 'Video',
+    thumbnailURL: posterSources[idx % posterSources.length],
   }
 })
-const mockVideos = shuffle(unshuffledVideos)
 
 export default mockVideos

+ 62 - 0
packages/app/src/mocking/server.ts

@@ -0,0 +1,62 @@
+import { createServer } from 'miragejs'
+import { createGraphQLHandler, mirageGraphQLFieldResolver } from '@miragejs/graphql'
+import { shuffle } from 'lodash'
+
+import schema from '../schema.graphql'
+import { mockChannels, mockVideos } from '@/mocking/data'
+
+createServer({
+  routes() {
+    const graphQLHandler = createGraphQLHandler(schema, this.schema, {
+      resolvers: {
+        Query: {
+          videos: (...params: unknown[]) => {
+            const videos = mirageGraphQLFieldResolver(...params)
+            return shuffle(videos)
+          },
+          featured_videos: (...params: unknown[]) => {
+            const videos = mirageGraphQLFieldResolver(...params)
+            return shuffle(videos)
+          },
+          channels: (...params: unknown[]) => {
+            const channels = mirageGraphQLFieldResolver(...params)
+            return shuffle(channels)
+          },
+        },
+      },
+    })
+
+    this.post('/graphql', graphQLHandler)
+  },
+
+  seeds(server) {
+    const channels = mockChannels.map((channel) => {
+      const models = server.create('Channel', {
+        ...channel,
+      })
+      return models
+    })
+
+    const location = server.schema.create('HTTPVideoMediaLocation', {
+      id: 'locationID',
+      host: 'https://js-video-example.s3.eu-central-1.amazonaws.com/waves.mp4',
+    })
+
+    const media = server.schema.create('VideoMedia', {
+      id: 'videoMediaID',
+      entityID: 'videoMediaEntityID',
+      encoding: 'H264_mpeg4',
+      pixelWidth: 1920,
+      pixelHeight: 1080,
+      location,
+    })
+
+    mockVideos.forEach((video, idx) =>
+      server.create('Video', {
+        ...video,
+        media,
+        channel: channels[idx % channels.length],
+      })
+    )
+  },
+})

+ 202 - 0
packages/app/src/schema.graphql

@@ -0,0 +1,202 @@
+scalar Date
+
+enum Language {
+  Chinese
+  English
+  Arabic
+  Portugese
+  French
+}
+
+type Channel {
+  id: ID!
+
+  # Id of underlying entity.
+  entityID: String!
+
+  handle: String!
+
+  description: String!
+
+  coverPhotoURL: String!
+
+  avatarPhotoURL: String!
+
+  isPublic: Boolean!
+
+  totalViews: Int!
+
+  isCurated: Boolean!
+
+  language: Language
+
+  videos: [Video!]
+}
+
+type Category {
+  id: ID!
+
+  # Id of underlying entity.
+  entityID: String!
+
+  name: String!
+
+  videos: [Video!]
+}
+
+type JoystreamVideoMediaLocation {
+  dataObjectID: String!
+}
+
+type HTTPVideoMediaLocation {
+  host: String!
+  port: Int
+}
+
+# In the future we can add IPFS, Dat, etc.
+union MediaLocation = JoystreamVideoMediaLocation | HTTPVideoMediaLocation
+
+# Mixed both encoding and containers, only having popular combos, may need to be changed later.
+enum VideoMediaEncoding {
+  H264_mpeg4
+  VP8_WEBM
+  Theroa_Vorbis
+}
+
+# Apparently there are lots of different Creative Commons licenses,
+# read about all here https://creativecommons.org/licenses/,
+# I haven't had the time.
+enum CreativeCommonsVersion {
+  CC_BY
+  CC_BY_SA
+  CC_BY_ND
+  CC_BY_NC
+  CC_BY_NC_SA
+  CC_BY_NC_ND
+}
+
+type CreativeCommonsLicense {
+  version: CreativeCommonsVersion
+}
+
+type UserDefinedLicense {
+  text: String!
+}
+
+union License = UserDefinedLicense | CreativeCommonsLicense
+
+type VideoMedia {
+  id: ID!
+
+  # Id of underlying entity.
+  entityID: String!
+
+  encoding: VideoMediaEncoding!
+
+  # Resolution width
+  pixelWidth: Int!
+
+  # Resolution height
+  pixelHeight: Int!
+
+  # Size in bytes
+  size: Float
+
+  # where to find
+  # FIXME: This should be MediaLocation instead. However, some issues arose when trying to implement this with a union type
+  # Let's keep it directly typed for now and resolve later
+  # See https://github.com/miragejs/graphql/issues/12
+  location: HTTPVideoMediaLocation!
+}
+
+type Video {
+  id: ID!
+
+  # Id of underlying entity.
+  entityID: String!
+
+  channel: Channel!
+
+  category: Category!
+
+  title: String!
+
+  description: String!
+
+  views: Int!
+
+  # In seconds
+  duration: Int!
+
+  # In intro
+  skippableIntroDuration: Int
+
+  thumbnailURL: String!
+  Language: Language
+
+  media: VideoMedia!
+
+  hasMarketing: Boolean
+
+  # Timestamp of block
+  publishedOnJoystreamAt: Date!
+  publishedOnJoystreamAtBlockHeight: Float!
+
+  # Possible time when video was published before Joystream
+  publishedBeforeJoystream: String
+
+  isPublic: Boolean!
+
+  isCurated: Boolean!
+
+  isExplicit: Boolean!
+
+  license: License!
+}
+
+union FreeTextSearchResultItemType = Video | Channel
+
+type FreeTextSearchResult {
+  item: FreeTextSearchResultItemType!
+
+  rank: Int!
+}
+
+type Query {
+  # Lookup a channel by its ID
+  channel(id: ID!): Channel
+
+  # List all channel by given constraints
+  channels(
+    order_by_creation_date: Boolean
+    ignore_curated: Boolean
+    ignore_non_public: Boolean
+    offset: Int
+    limit: Int
+  ): [Channel!]!
+
+  # Lookup a channel by its ID
+  category(id: ID!): Category
+
+  # List all categories
+  categories: [Category!]!
+
+  # Lookup video by its ID
+  video(id: ID!): Video
+
+  # List all videos by given constraints
+  videos(
+    order_by_publication_date: Boolean
+    ignore_curated: Boolean
+    ignore_non_public: Boolean
+    in_category_with_ID: ID
+    offset: Int
+    limit: Int
+  ): [Video!]!
+
+  # List all top trending videos
+  featured_videos: [Video!]!
+
+  # Free text search across videos and channels
+  search(query_string: String!): [FreeTextSearchResult!]!
+}

+ 2 - 2
packages/app/src/shared/components/Avatar/Avatar.style.tsx

@@ -2,7 +2,7 @@ import { makeStyles, StyleFn } from '../../utils'
 import { colors, spacing } from '../../theme'
 
 export type AvatarStyleProps = {
-  size: 'small' | 'default' | 'large'
+  size?: 'small' | 'default' | 'large'
 }
 
 const container: StyleFn = (_, { size = 'default' }) => {
@@ -31,7 +31,7 @@ const img: StyleFn = () => ({
   borderRadius: 999,
 })
 
-export const useCSS = (props: Partial<AvatarStyleProps>) => ({
+export const useCSS = (props: AvatarStyleProps) => ({
   container: makeStyles([container])(props),
   img: makeStyles([img])(props),
 })

+ 4 - 11
packages/app/src/shared/components/Avatar/Avatar.tsx

@@ -3,10 +3,10 @@ import { SerializedStyles } from '@emotion/core'
 import { AvatarStyleProps, useCSS } from './Avatar.style'
 
 export type AvatarProps = {
-  onClick: (e: React.MouseEvent<HTMLElement>) => void
+  onClick?: (e: React.MouseEvent<HTMLElement>) => void
   outerStyles?: SerializedStyles
-  img: string
-  name: string
+  img?: string | null
+  name?: string
   className?: string
 } & AvatarStyleProps
 
@@ -16,14 +16,7 @@ function initialsFromName(name: string): string {
   return vowels.includes(second) ? first : `${first}${second}`
 }
 
-const Avatar: React.FC<Partial<AvatarProps>> = ({
-  img,
-  outerStyles,
-  onClick = () => {},
-  name,
-  className,
-  ...styleProps
-}) => {
+const Avatar: React.FC<AvatarProps> = ({ img, outerStyles, onClick = () => {}, name, className, ...styleProps }) => {
   const styles = useCSS({ ...styleProps })
   return (
     <div css={[styles.container, outerStyles]} className={className} onClick={onClick}>

+ 5 - 6
packages/app/src/shared/components/VideoPreview/VideoPreview.tsx

@@ -15,16 +15,15 @@ import {
   TextContainer,
   TitleHeader,
 } from './VideoPreview.styles'
-import { DateTime, Duration } from 'luxon'
-import { formatDateShort, formatDurationShort } from '@/utils/time'
+import { formatDateAgo, formatDurationShort } from '@/utils/time'
 import { formatNumberShort } from '@/utils/number'
 
 type VideoPreviewProps = {
   title: string
   channelName: string
-  channelAvatarURL?: string
-  createdAt: DateTime
-  duration?: Duration
+  channelAvatarURL?: string | null
+  createdAt: Date
+  duration?: number
   // video watch progress in percent (0-100)
   progress?: number
   views: number
@@ -106,7 +105,7 @@ const VideoPreview: React.FC<VideoPreviewProps> = ({
           )}
           {showMeta && (
             <MetaText>
-              {formatDateShort(createdAt)}・{formatNumberShort(views)} views
+              {formatDateAgo(createdAt)}・{formatNumberShort(views)} views
             </MetaText>
           )}
         </TextContainer>

+ 0 - 8
packages/app/src/types/channel.ts

@@ -1,8 +0,0 @@
-type Channel = {
-  id: string
-  name: string
-  avatarURL?: string
-  views: number
-}
-
-export default Channel

+ 6 - 0
packages/app/src/types/graphql.d.ts

@@ -0,0 +1,6 @@
+declare module '*.graphql' {
+  import { DocumentNode } from 'graphql'
+  const Schema: DocumentNode
+
+  export = Schema
+}

+ 1 - 0
packages/app/src/types/graphqlScalars.d.ts

@@ -0,0 +1 @@
+type GQLDate = Date

+ 1 - 0
packages/app/src/types/mirage.d.ts

@@ -0,0 +1 @@
+declare module '@miragejs/graphql'

+ 0 - 17
packages/app/src/types/video.ts

@@ -1,17 +0,0 @@
-import { DateTime, Duration } from 'luxon'
-import Channel from './channel'
-import VideoMedia from './videoMedia'
-
-type Video = {
-  id: string
-  title: string
-  description: string
-  views: number
-  createdAt: DateTime
-  duration: Duration
-  posterURL: string
-  media: VideoMedia
-  channel: Channel
-}
-
-export default Video

+ 0 - 7
packages/app/src/types/videoMedia.ts

@@ -1,7 +0,0 @@
-type VideoMedia = {
-  pixelWidth: number
-  pixelHeight: number
-  location: string
-}
-
-export default VideoMedia

+ 0 - 5
packages/app/src/utils/date.ts

@@ -1,5 +0,0 @@
-import { DateTime } from 'luxon'
-
-export const formatDateShort = (date: DateTime): string => {
-  return date.setLocale('en-gb').toLocaleString(DateTime.DATE_MED)
-}

+ 24 - 6
packages/app/src/utils/time.ts

@@ -1,10 +1,28 @@
-import { DateTime, Duration } from 'luxon'
+import { formatDistanceToNowStrict } from 'date-fns'
 
-export const formatDateShort = (date: DateTime): string => {
-  return date.setLocale('en-gb').toLocaleString(DateTime.DATE_MED)
+export const formatDateAgo = (date: Date): string => {
+  return `${formatDistanceToNowStrict(date)} ago`
 }
 
-export const formatDurationShort = (duration: Duration): string => {
-  const format = duration.as('hours') >= 1 ? 'h:mm:ss' : 'mm:ss'
-  return duration.toFormat(format)
+export const formatDurationShort = (duration: number): string => {
+  const MINUTES_IN_HOUR = 60
+  const SECONDS_IN_HOUR = MINUTES_IN_HOUR * 60
+
+  const normalize = (n: number) => n.toString().padStart(2, '0')
+
+  let remaining = duration
+
+  const hours = Math.floor(remaining / SECONDS_IN_HOUR)
+  remaining = remaining % SECONDS_IN_HOUR
+
+  const minutes = Math.floor(remaining / MINUTES_IN_HOUR)
+  remaining = remaining % MINUTES_IN_HOUR
+
+  const seconds = remaining
+
+  if (hours) {
+    return `${hours}:${normalize(minutes)}:${normalize(seconds)}`
+  }
+
+  return `${minutes}:${normalize(seconds)}`
 }

+ 38 - 18
packages/app/src/views/HomeView.tsx

@@ -2,26 +2,46 @@ import { css } from '@emotion/core'
 import React from 'react'
 import { ChannelGallery, Hero, Main, VideoGallery } from '@/components'
 import { RouteComponentProps } from '@reach/router'
+import { useQuery } from '@apollo/client'
+import { GET_FEATURED_VIDEOS, GET_NEWEST_VIDEOS } from '@/api/queries'
+import { GetNewestVideos } from '@/api/queries/__generated__/GetNewestVideos'
+import { GetFeaturedVideos } from '@/api/queries/__generated__/GetFeaturedVideos'
+import { GetNewestChannels } from '@/api/queries/__generated__/GetNewestChannels'
+import { GET_NEWEST_CHANNELS } from '@/api/queries/channels'
 
 const backgroundImg = 'https://source.unsplash.com/Nyvq2juw4_o/1920x1080'
 
-const HomeView: React.FC<RouteComponentProps> = () => (
-  <>
-    <Hero backgroundImg={backgroundImg} />
-    <Main
-      containerCss={css`
-        margin: 1rem 0;
-        & > * {
-          margin-bottom: 3rem;
-        }
-      `}
-    >
-      <VideoGallery title="Newest videos" />
-      <VideoGallery title="Featured videos" />
-      <ChannelGallery title="Newest channels" />
-      {/*  infinite video loader */}
-    </Main>
-  </>
-)
+const HomeView: React.FC<RouteComponentProps> = () => {
+  const { loading: newestVideosLoading, data: newestVideosData } = useQuery<GetNewestVideos>(GET_NEWEST_VIDEOS)
+  const { loading: featuredVideosLoading, data: featuredVideosData } = useQuery<GetFeaturedVideos>(GET_FEATURED_VIDEOS)
+  const { loading: newestChannelsLoading, data: newestChannelsData } = useQuery<GetNewestChannels>(GET_NEWEST_CHANNELS)
+
+  return (
+    <>
+      <Hero backgroundImg={backgroundImg} />
+      <Main
+        containerCss={css`
+          margin: 1rem 0;
+          & > * {
+            margin-bottom: 3rem;
+          }
+        `}
+      >
+        <VideoGallery title="Newest videos" loading={newestVideosLoading} videos={newestVideosData?.videos} />
+        <VideoGallery
+          title="Featured videos"
+          loading={featuredVideosLoading}
+          videos={featuredVideosData?.featured_videos} // eslint-disable-line camelcase
+        />
+        <ChannelGallery
+          title="Newest channels"
+          loading={newestChannelsLoading}
+          channels={newestChannelsData?.channels}
+        />
+        {/*  infinite video loader */}
+      </Main>
+    </>
+  )
+}
 
 export default HomeView

+ 20 - 10
packages/app/src/views/VideoView/VideoView.tsx

@@ -16,15 +16,25 @@ import {
   TitleActionsContainer,
 } from './VideoView.style'
 import { Button, VideoPlayer } from '@/shared/components'
-import { formatDateShort } from '@/utils/time'
+import { formatDateAgo } from '@/utils/time'
 import { formatNumber } from '@/utils/number'
-import { mockVideos } from '@/config/mocks'
+import { useQuery } from '@apollo/client'
+import { GET_NEWEST_VIDEOS } from '@/api/queries'
+import { GetNewestVideos } from '@/api/queries/__generated__/GetNewestVideos'
 
 const VideoView: React.FC<RouteComponentProps> = () => {
-  const { title, views, createdAt, channel, description } = mockVideos[0]
+  const { loading, data } = useQuery<GetNewestVideos>(GET_NEWEST_VIDEOS)
+
+  if (loading || !data) {
+    return <p>Loading</p>
+  }
+
+  const { title, views, publishedOnJoystreamAt, channel, description } = data.videos[0]
 
   const descriptionLines = description.split('\n')
 
+  const moreVideos = Array.from({ length: 10 }, () => data.videos[0])
+
   return (
     <Container>
       <PlayerContainer>
@@ -38,26 +48,26 @@ const VideoView: React.FC<RouteComponentProps> = () => {
           </ActionsContainer>
         </TitleActionsContainer>
         <Meta>
-          {formatNumber(views)} views • {formatDateShort(createdAt)}
+          {formatNumber(views)} views • {formatDateAgo(publishedOnJoystreamAt)}
         </Meta>
-        <StyledChannelAvatar {...channel} />
+        <StyledChannelAvatar name={channel.handle} avatarUrl={channel.avatarPhotoURL} />
         <DescriptionContainer>
           {descriptionLines.map((line, idx) => (
             <p key={idx}>{line}</p>
           ))}
         </DescriptionContainer>
         <MoreVideosContainer>
-          <MoreVideosHeader>More from {channel.name}</MoreVideosHeader>
+          <MoreVideosHeader>More from {channel.handle}</MoreVideosHeader>
           <MoreVideosGrid>
-            {mockVideos.map((v, idx) => (
+            {moreVideos.map((v, idx) => (
               <MoreVideosPreview
                 key={idx}
                 title={v.title}
-                channelName={v.channel.name}
-                createdAt={v.createdAt}
+                channelName={v.channel.handle}
+                createdAt={v.publishedOnJoystreamAt}
                 duration={v.duration}
                 views={v.views}
-                posterURL={v.posterURL}
+                posterURL={v.thumbnailURL}
               />
             ))}
           </MoreVideosGrid>

File diff suppressed because it is too large
+ 746 - 15
yarn.lock


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