Browse Source

merge channel view, player fixes and various tweaks

merge channel view, player fixes and various tweaks
Klaudiusz Dembler 3 years ago
parent
commit
0ef542dabe
100 changed files with 1160 additions and 537 deletions
  1. 0 0
      docs/video-hero.md
  2. 4 2
      src/MainLayout.tsx
  3. 8 10
      src/api/client/cache.ts
  4. 212 37
      src/api/client/resolvers.ts
  5. 16 2
      src/api/client/transforms/index.ts
  6. 38 0
      src/api/client/transforms/orionFollows.ts
  7. 83 3
      src/api/client/transforms/orionViews.ts
  8. 24 0
      src/api/client/transforms/queryNodeViews.ts
  9. 2 1
      src/api/hooks/channel.ts
  10. 3 0
      src/api/queries/__generated__/baseTypes.generated.ts
  11. 173 2
      src/api/queries/__generated__/channels.generated.tsx
  12. 4 2
      src/api/queries/__generated__/search.generated.tsx
  13. 29 3
      src/api/queries/channels.graphql
  14. 2 2
      src/api/queries/search.graphql
  15. 2 1
      src/api/schemas/extendedQueryNode.graphql
  16. 5 5
      src/components/ChannelCard.tsx
  17. 3 3
      src/components/ChannelGallery.tsx
  18. 3 3
      src/components/ChannelGrid.tsx
  19. 2 2
      src/components/ChannelLink/ChannelLink.style.ts
  20. 2 2
      src/components/ChannelLink/ChannelLink.tsx
  21. 0 1
      src/components/CoverVideo/index.ts
  22. 40 7
      src/components/Dialogs/ActionDialog/ActionDialog.stories.tsx
  23. 0 62
      src/components/Dialogs/ActionDialog/ActionDialog.style.ts
  24. 20 32
      src/components/Dialogs/ActionDialog/ActionDialog.tsx
  25. 1 0
      src/components/Dialogs/BaseDialog/BaseDialog.style.ts
  26. 4 4
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.stories.tsx
  27. 2 2
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.style.ts
  28. 1 2
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx
  29. 1 2
      src/components/Dialogs/ImageCropDialog/cropper.ts
  30. 1 3
      src/components/Dialogs/TransactionDialog/TransactionDialog.tsx
  31. 3 3
      src/components/InfiniteGrids/InfiniteChannelGrid.tsx
  32. 5 5
      src/components/InfiniteGrids/InfiniteVideoGrid.tsx
  33. 3 3
      src/components/LimitedWidthContainer.tsx
  34. 0 18
      src/components/PlaceholderVideoGrid.tsx
  35. 18 0
      src/components/SkeletonLoaderVideoGrid.tsx
  36. 2 2
      src/components/Topbar/StudioTopbar/StudioTopbar.style.tsx
  37. 7 7
      src/components/Topbar/StudioTopbar/StudioTopbar.tsx
  38. 6 6
      src/components/VideoGallery.tsx
  39. 3 3
      src/components/VideoGrid.tsx
  40. 4 20
      src/components/VideoHero/VideoHero.style.ts
  41. 10 13
      src/components/VideoHero/VideoHero.tsx
  42. 3 3
      src/components/VideoHero/VideoHeroData.ts
  43. 0 0
      src/components/VideoHero/backupVideoHeroInfo.json
  44. 1 0
      src/components/VideoHero/index.ts
  45. 14 19
      src/components/VideoTile.tsx
  46. 5 5
      src/components/index.ts
  47. 6 0
      src/config/sorting.ts
  48. 11 7
      src/hooks/useDeleteVideo.tsx
  49. 12 8
      src/hooks/useDisplayDataLostWarning.tsx
  50. 2 2
      src/mocking/accessors/filtering.ts
  51. 15 0
      src/mocking/accessors/orion.ts
  52. 4 4
      src/mocking/data/mockChannels.ts
  53. 8 8
      src/mocking/data/mockVideos.ts
  54. 0 0
      src/mocking/data/raw/VideoHero.json
  55. 9 0
      src/mocking/handlers.ts
  56. 5 0
      src/mocking/store.ts
  57. 5 1
      src/mocking/types.ts
  58. 9 5
      src/providers/transactionManager/useTransaction.ts
  59. 2 2
      src/shared/components/ActionBar/ActionBarTransaction.style.ts
  60. 5 5
      src/shared/components/ActionBar/ActionBarTransaction.tsx
  61. 16 3
      src/shared/components/Avatar/Avatar.style.tsx
  62. 2 2
      src/shared/components/Avatar/Avatar.tsx
  63. 2 2
      src/shared/components/CategoryPicker/CategoryPicker.style.ts
  64. 2 2
      src/shared/components/CategoryPicker/CategoryPicker.tsx
  65. 6 6
      src/shared/components/ChannelCardBase/ChannelCard.stories.tsx
  66. 0 0
      src/shared/components/ChannelCardBase/ChannelCardBase.style.tsx
  67. 8 8
      src/shared/components/ChannelCardBase/ChannelCardBase.tsx
  68. 1 0
      src/shared/components/ChannelCardBase/index.ts
  69. 3 93
      src/shared/components/ChannelCover/ChannelCover.style.ts
  70. 1 1
      src/shared/components/ChannelCover/ChannelCover.tsx
  71. 0 1
      src/shared/components/ChannelPreviewBase/index.ts
  72. 0 1
      src/shared/components/Checkout/index.tsx
  73. 7 7
      src/shared/components/CircularProgress/CircularProgress.stories.tsx
  74. 0 0
      src/shared/components/CircularProgress/CircularProgress.style.tsx
  75. 4 4
      src/shared/components/CircularProgress/CircularProgress.tsx
  76. 1 1
      src/shared/components/CircularProgress/Path.tsx
  77. 1 0
      src/shared/components/CircularProgress/index.ts
  78. 0 1
      src/shared/components/CircularProgressbar/index.ts
  79. 5 5
      src/shared/components/DismissibleBanner/DismissibleBanner.stories.tsx
  80. 2 2
      src/shared/components/DismissibleBanner/DismissibleBanner.tsx
  81. 1 0
      src/shared/components/DismissibleBanner/index.ts
  82. 0 1
      src/shared/components/DismissibleMessage/index.ts
  83. 38 0
      src/shared/components/EmptyFallback/EmptyFallback.stories.tsx
  84. 40 0
      src/shared/components/EmptyFallback/EmptyFallback.styles.ts
  85. 40 0
      src/shared/components/EmptyFallback/EmptyFallback.tsx
  86. 1 0
      src/shared/components/EmptyFallback/index.ts
  87. 2 2
      src/shared/components/FileStep/FileStep.style.ts
  88. 2 2
      src/shared/components/Grid/Grid.tsx
  89. 0 1
      src/shared/components/HeaderTextField/index.ts
  90. 0 32
      src/shared/components/Placeholder/Placeholder.tsx
  91. 0 1
      src/shared/components/Placeholder/index.ts
  92. 5 5
      src/shared/components/ProgressDrawer/ProgressDrawer.stories.tsx
  93. 2 2
      src/shared/components/ProgressDrawer/ProgressDrawer.styles.tsx
  94. 5 5
      src/shared/components/ProgressDrawer/ProgressDrawer.tsx
  95. 1 0
      src/shared/components/ProgressDrawer/index.tsx
  96. 1 1
      src/shared/components/Select/Select.style.ts
  97. 31 0
      src/shared/components/SkeletonLoader/SkeletonLoader.stories.tsx
  98. 62 0
      src/shared/components/SkeletonLoader/SkeletonLoader.tsx
  99. 1 0
      src/shared/components/SkeletonLoader/index.ts
  100. 5 2
      src/shared/components/Tabs/Tabs.styles.tsx

+ 0 - 0
docs/cover-video.md → docs/video-hero.md


+ 4 - 2
src/MainLayout.tsx

@@ -29,8 +29,10 @@ export const MainLayout: React.FC = () => {
     description:
     description:
       'It seems the browser version you are using is not fully supported by Joystream. Some of the features may be broken or not accessible. For the best experience, please upgrade your browser to the latest version.',
       'It seems the browser version you are using is not fully supported by Joystream. Some of the features may be broken or not accessible. For the best experience, please upgrade your browser to the latest version.',
     variant: 'warning',
     variant: 'warning',
-    primaryButtonText: 'Click here to see instructions',
-    onPrimaryButtonClick: () => window.open('https://www.whatismybrowser.com/guides/how-to-update-your-browser/auto'),
+    primaryButton: {
+      text: 'Click here to see instructions',
+      onClick: () => window.open('https://www.whatismybrowser.com/guides/how-to-update-your-browser/auto'),
+    },
     onExitClick: () => closeDialog(),
     onExitClick: () => closeDialog(),
   })
   })
 
 

+ 8 - 10
src/api/client/cache.ts

@@ -8,28 +8,29 @@ import {
   AllChannelFieldsFragment,
   AllChannelFieldsFragment,
   AssetAvailability,
   AssetAvailability,
   GetVideosConnectionQueryVariables,
   GetVideosConnectionQueryVariables,
-  GetVideosQueryVariables,
   Query,
   Query,
   VideoConnection,
   VideoConnection,
   VideoFieldsFragment,
   VideoFieldsFragment,
   VideoOrderByInput,
   VideoOrderByInput,
 } from '../queries'
 } from '../queries'
 
 
-const getVideoKeyArgs = (args: Record<string, GetVideosQueryVariables['where']> | null) => {
+const getVideoKeyArgs = (args: GetVideosConnectionQueryVariables | null) => {
   // make sure queries asking for a specific category are separated in cache
   // make sure queries asking for a specific category are separated in cache
+  const onlyCount = args?.first === 0
   const channelId = args?.where?.channelId_eq || ''
   const channelId = args?.where?.channelId_eq || ''
   const categoryId = args?.where?.categoryId_eq || ''
   const categoryId = args?.where?.categoryId_eq || ''
   const idEq = args?.where?.id_eq || ''
   const idEq = args?.where?.id_eq || ''
   const isPublic = args?.where?.isPublic_eq ?? ''
   const isPublic = args?.where?.isPublic_eq ?? ''
   const channelIdIn = args?.where?.channelId_in ? JSON.stringify(args.where.channelId_in) : ''
   const channelIdIn = args?.where?.channelId_in ? JSON.stringify(args.where.channelId_in) : ''
   const createdAtGte = args?.where?.createdAt_gte ? JSON.stringify(args.where.createdAt_gte) : ''
   const createdAtGte = args?.where?.createdAt_gte ? JSON.stringify(args.where.createdAt_gte) : ''
+  const sorting = args?.orderBy?.[0] ? args.orderBy[0] : ''
 
 
   // only for counting videos in HomeView
   // only for counting videos in HomeView
   if (args?.where?.channelId_in && !args?.first) {
   if (args?.where?.channelId_in && !args?.first) {
     return `${createdAtGte}:${channelIdIn}`
     return `${createdAtGte}:${channelIdIn}`
   }
   }
 
 
-  return `${channelId}:${categoryId}:${channelIdIn}:${createdAtGte}:${isPublic}:${idEq}`
+  return `${onlyCount}:${channelId}:${categoryId}:${channelIdIn}:${createdAtGte}:${isPublic}:${idEq}:${sorting}`
 }
 }
 
 
 const createDateHandler = () => ({
 const createDateHandler = () => ({
@@ -96,13 +97,10 @@ const queryCacheFields: CachePolicyFields<keyof Query> = {
       const filteredEdges =
       const filteredEdges =
         existing?.edges.filter((edge) => readField('isPublic', edge.node) === isPublic || isPublic === undefined) ?? []
         existing?.edges.filter((edge) => readField('isPublic', edge.node) === isPublic || isPublic === undefined) ?? []
 
 
-      const sortingASC = args?.orderBy === VideoOrderByInput.CreatedAtAsc
-      const preSortedDESC = (filteredEdges || [])
-        .slice()
-        .sort(
-          (a, b) =>
-            (readField('createdAt', b.node) as Date).getTime() - (readField('createdAt', a.node) as Date).getTime()
-        )
+      const sortingASC = args?.orderBy?.[0] === VideoOrderByInput.CreatedAtAsc
+      const preSortedDESC = (filteredEdges || []).slice().sort((a, b) => {
+        return (readField('createdAt', b.node) as Date).getTime() - (readField('createdAt', a.node) as Date).getTime()
+      })
       const sortedEdges = sortingASC ? preSortedDESC.reverse() : preSortedDESC
       const sortedEdges = sortingASC ? preSortedDESC.reverse() : preSortedDESC
 
 
       return (
       return (

+ 212 - 37
src/api/client/resolvers.ts

@@ -6,28 +6,43 @@ import { createLookup } from '@/utils/data'
 import { Logger } from '@/utils/logger'
 import { Logger } from '@/utils/logger'
 
 
 import {
 import {
+  ORION_BATCHED_FOLLOWS_QUERY_NAME,
   ORION_BATCHED_VIEWS_QUERY_NAME,
   ORION_BATCHED_VIEWS_QUERY_NAME,
   ORION_FOLLOWS_QUERY_NAME,
   ORION_FOLLOWS_QUERY_NAME,
+  ORION_VIEWS_QUERY_NAME,
   RemoveQueryNodeFollowsField,
   RemoveQueryNodeFollowsField,
   RemoveQueryNodeViewsField,
   RemoveQueryNodeViewsField,
+  TransformBatchedOrionFollowsField,
   TransformBatchedOrionViewsField,
   TransformBatchedOrionViewsField,
   TransformOrionFollowsField,
   TransformOrionFollowsField,
+  TransformOrionViewsField,
 } from './transforms'
 } from './transforms'
-import { ORION_VIEWS_QUERY_NAME, TransformOrionViewsField } from './transforms/orionViews'
+import {
+  ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
+  ORION_CHANNEL_VIEWS_QUERY_NAME,
+  TransformBatchedChannelOrionViewsField,
+  TransformOrionChannelViewsField,
+} from './transforms/orionViews'
+import { RemoveQueryNodeChannelViewsField } from './transforms/queryNodeViews'
+
+import { Channel, ChannelEdge, Video, VideoEdge } from '../queries'
 
 
-import { VideoEdge } from '../queries'
+const BATCHED_VIDEO_VIEWS_QUERY_NAME = 'GetBatchedVideoViews'
+const BATCHED_CHANNEL_VIEWS_QUERY_NAME = 'GetBatchedChannelViews'
+const BATCHED_FOLLOWS_VIEWS_QUERY_NAME = 'GetBatchedChannelFollows'
 
 
 const createResolverWithTransforms = (
 const createResolverWithTransforms = (
   schema: GraphQLSchema,
   schema: GraphQLSchema,
   fieldName: string,
   fieldName: string,
-  transforms: Array<Transform>
+  transforms: Array<Transform>,
+  operationName?: string
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
 ): ISchemaLevelResolver<any, any> => {
 ): ISchemaLevelResolver<any, any> => {
   return async (parent, args, context, info) =>
   return async (parent, args, context, info) =>
     delegateToSchema({
     delegateToSchema({
       schema,
       schema,
       operation: 'query',
       operation: 'query',
-      operationName: info?.operation?.name?.value,
+      operationName: operationName ? operationName : info?.operation?.name?.value,
       fieldName,
       fieldName,
       args,
       args,
       context,
       context,
@@ -45,15 +60,96 @@ export const queryNodeStitchingResolvers = (
     videoByUniqueInput: createResolverWithTransforms(queryNodeSchema, 'videoByUniqueInput', [
     videoByUniqueInput: createResolverWithTransforms(queryNodeSchema, 'videoByUniqueInput', [
       RemoveQueryNodeViewsField,
       RemoveQueryNodeViewsField,
     ]),
     ]),
-    videos: createResolverWithTransforms(queryNodeSchema, 'videos', [RemoveQueryNodeViewsField]),
+    videos: async (parent, args, context, info) => {
+      try {
+        const videosResolver = createResolverWithTransforms(queryNodeSchema, 'videos', [RemoveQueryNodeViewsField])
+        const videos = await videosResolver(parent, args, context, info)
+
+        const batchedVideoViewsResolver = createResolverWithTransforms(
+          orionSchema,
+          ORION_BATCHED_VIEWS_QUERY_NAME,
+          [TransformBatchedOrionViewsField],
+          // operationName has to be manually kept in sync with the query name used
+          BATCHED_VIDEO_VIEWS_QUERY_NAME
+        )
+        const batchedVideoViews = await batchedVideoViewsResolver(
+          parent,
+          {
+            videoIdList: videos.map((video: Video) => video.id),
+          },
+          context,
+          info
+        )
+
+        const viewsLookup = createLookup<{ id: string; views: number }>(batchedVideoViews || [])
+        return videos.map((video: Video) => ({ ...video, views: viewsLookup[video.id]?.views || 0 }))
+      } catch (error) {
+        Logger.warn('Failed to resolve videos field', { error })
+        return null
+      }
+    },
     videosConnection: createResolverWithTransforms(queryNodeSchema, 'videosConnection', [RemoveQueryNodeViewsField]),
     videosConnection: createResolverWithTransforms(queryNodeSchema, 'videosConnection', [RemoveQueryNodeViewsField]),
     // channel queries
     // channel queries
     channelByUniqueInput: createResolverWithTransforms(queryNodeSchema, 'channelByUniqueInput', [
     channelByUniqueInput: createResolverWithTransforms(queryNodeSchema, 'channelByUniqueInput', [
       RemoveQueryNodeFollowsField,
       RemoveQueryNodeFollowsField,
+      RemoveQueryNodeChannelViewsField,
     ]),
     ]),
-    channels: createResolverWithTransforms(queryNodeSchema, 'channels', [RemoveQueryNodeFollowsField]),
+    channels: async (parent, args, context, info) => {
+      try {
+        const channelsResolver = createResolverWithTransforms(queryNodeSchema, 'channels', [
+          RemoveQueryNodeFollowsField,
+          RemoveQueryNodeChannelViewsField,
+        ])
+        const channels = await channelsResolver(parent, args, context, info)
+
+        const batchedChannelFollowsResolver = createResolverWithTransforms(
+          orionSchema,
+          ORION_BATCHED_FOLLOWS_QUERY_NAME,
+          [TransformBatchedOrionFollowsField],
+          // operationName has to be manually kept in sync with the query name used
+          BATCHED_FOLLOWS_VIEWS_QUERY_NAME
+        )
+        const batchedChannelFollows = await batchedChannelFollowsResolver(
+          parent,
+          {
+            channelIdList: channels.map((channel: Channel) => channel.id),
+          },
+          context,
+          info
+        )
+        const followsLookup = createLookup<{ id: string; follows: number }>(batchedChannelFollows || [])
+
+        const batchedChannelViewsResolver = createResolverWithTransforms(
+          orionSchema,
+          ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
+          [TransformBatchedChannelOrionViewsField],
+          // operationName has to be manually kept in sync with the query name used
+          BATCHED_CHANNEL_VIEWS_QUERY_NAME
+        )
+        const batchedChannelViews = await batchedChannelViewsResolver(
+          parent,
+          {
+            channelIdList: channels.map((channel: Channel) => channel.id),
+          },
+          context,
+          info
+        )
+
+        const viewsLookup = createLookup<{ id: string; views: number }>(batchedChannelViews || [])
+
+        return channels.map((channel: Channel) => ({
+          ...channel,
+          follows: followsLookup[channel.id]?.follows || 0,
+          views: viewsLookup[channel.id]?.views || 0,
+        }))
+      } catch (error) {
+        Logger.warn('Failed to resolve channels field', { error })
+        return null
+      }
+    },
     channelsConnection: createResolverWithTransforms(queryNodeSchema, 'channelsConnection', [
     channelsConnection: createResolverWithTransforms(queryNodeSchema, 'channelsConnection', [
       RemoveQueryNodeFollowsField,
       RemoveQueryNodeFollowsField,
+      RemoveQueryNodeChannelViewsField,
     ]),
     ]),
     // mixed queries
     // mixed queries
     search: createResolverWithTransforms(queryNodeSchema, 'search', [
     search: createResolverWithTransforms(queryNodeSchema, 'search', [
@@ -63,23 +159,25 @@ export const queryNodeStitchingResolvers = (
   },
   },
   Video: {
   Video: {
     views: async (parent, args, context, info) => {
     views: async (parent, args, context, info) => {
-      if (parent.views != null) {
+      if (parent.views !== undefined) {
         return parent.views
         return parent.views
       }
       }
+      const orionViewsResolver = createResolverWithTransforms(
+        orionSchema,
+        ORION_VIEWS_QUERY_NAME,
+        [TransformOrionViewsField],
+        // operationName has to be manually kept in sync with the query name used
+        'GetVideoViews'
+      )
       try {
       try {
-        return await delegateToSchema({
-          schema: orionSchema,
-          operation: 'query',
-          // operationName has to be manually kept in sync with the query name used
-          operationName: 'GetVideoViews',
-          fieldName: ORION_VIEWS_QUERY_NAME,
-          args: {
+        return await orionViewsResolver(
+          parent,
+          {
             videoId: parent.id,
             videoId: parent.id,
           },
           },
           context,
           context,
-          info,
-          transforms: [TransformOrionViewsField],
-        })
+          info
+        )
       } catch (error) {
       } catch (error) {
         Logger.warn('Failed to resolve views field', { error })
         Logger.warn('Failed to resolve views field', { error })
         return null
         return null
@@ -88,19 +186,21 @@ export const queryNodeStitchingResolvers = (
   },
   },
   VideoConnection: {
   VideoConnection: {
     edges: async (parent, args, context, info) => {
     edges: async (parent, args, context, info) => {
-      const batchedVideoViews = await delegateToSchema({
-        schema: orionSchema,
-        operation: 'query',
+      const batchedVideoViewsResolver = createResolverWithTransforms(
+        orionSchema,
+        ORION_BATCHED_VIEWS_QUERY_NAME,
+        [TransformBatchedOrionViewsField],
         // operationName has to be manually kept in sync with the query name used
         // operationName has to be manually kept in sync with the query name used
-        operationName: 'GetBatchedVideoViews',
-        fieldName: ORION_BATCHED_VIEWS_QUERY_NAME,
-        args: {
+        BATCHED_VIDEO_VIEWS_QUERY_NAME
+      )
+      const batchedVideoViews = await batchedVideoViewsResolver(
+        parent,
+        {
           videoIdList: parent.edges.map((edge: VideoEdge) => edge.node.id),
           videoIdList: parent.edges.map((edge: VideoEdge) => edge.node.id),
         },
         },
         context,
         context,
-        info,
-        transforms: [TransformBatchedOrionViewsField],
-      })
+        info
+      )
 
 
       const viewsLookup = createLookup<{ id: string; views: number }>(batchedVideoViews || [])
       const viewsLookup = createLookup<{ id: string; views: number }>(batchedVideoViews || [])
 
 
@@ -113,27 +213,102 @@ export const queryNodeStitchingResolvers = (
       }))
       }))
     },
     },
   },
   },
-
   Channel: {
   Channel: {
+    views: async (parent, args, context, info) => {
+      if (parent.views != null) {
+        return parent.views
+      }
+      const orionViewsResolver = createResolverWithTransforms(
+        orionSchema,
+        ORION_CHANNEL_VIEWS_QUERY_NAME,
+        [TransformOrionChannelViewsField],
+        // operationName has to be manually kept in sync with the query name used
+        'GetChannelViews'
+      )
+      try {
+        return await orionViewsResolver(
+          parent,
+          {
+            channelId: parent.id,
+          },
+          context,
+          info
+        )
+      } catch (error) {
+        Logger.warn('Failed to resolve views field', { error })
+        return null
+      }
+    },
     follows: async (parent, args, context, info) => {
     follows: async (parent, args, context, info) => {
+      if (parent.follows !== undefined) {
+        return parent.follows
+      }
+      const orionFollowsResolver = createResolverWithTransforms(
+        orionSchema,
+        ORION_FOLLOWS_QUERY_NAME,
+        [TransformOrionFollowsField],
+        'GetChannelFollows'
+      )
       try {
       try {
-        return await delegateToSchema({
-          schema: orionSchema,
-          operation: 'query',
-          // operationName has to be manually kept in sync with the query name used
-          operationName: 'GetChannelFollows',
-          fieldName: ORION_FOLLOWS_QUERY_NAME,
-          args: {
+        return await orionFollowsResolver(
+          parent,
+          {
             channelId: parent.id,
             channelId: parent.id,
           },
           },
           context,
           context,
-          info,
-          transforms: [TransformOrionFollowsField],
-        })
+          info
+        )
       } catch (error) {
       } catch (error) {
         Logger.warn('Failed to resolve follows field', { error })
         Logger.warn('Failed to resolve follows field', { error })
         return null
         return null
       }
       }
     },
     },
   },
   },
+  ChannelConnection: {
+    edges: async (parent, args, context, info) => {
+      const batchedChannelFollowsResolver = createResolverWithTransforms(
+        orionSchema,
+        ORION_BATCHED_FOLLOWS_QUERY_NAME,
+        [TransformBatchedOrionFollowsField],
+        // operationName has to be manually kept in sync with the query name used
+        BATCHED_FOLLOWS_VIEWS_QUERY_NAME
+      )
+      const batchedChannelFollows = await batchedChannelFollowsResolver(
+        parent,
+        {
+          channelIdList: parent.edges.map((edge: ChannelEdge) => edge.node.id),
+        },
+        context,
+        info
+      )
+
+      const batchedChannelViewsResolver = createResolverWithTransforms(
+        orionSchema,
+        ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
+        [TransformBatchedChannelOrionViewsField],
+        // operationName has to be manually kept in sync with the query name used
+        BATCHED_CHANNEL_VIEWS_QUERY_NAME
+      )
+      const batchedChannelViews = await batchedChannelViewsResolver(
+        parent,
+        {
+          channelIdList: parent.edges.map((edge: ChannelEdge) => edge.node.id),
+        },
+        context,
+        info
+      )
+
+      const followsLookup = createLookup<{ id: string; follows: number }>(batchedChannelFollows || [])
+      const viewsLookup = createLookup<{ id: string; views: number }>(batchedChannelViews || [])
+
+      return parent.edges.map((edge: ChannelEdge) => ({
+        ...edge,
+        node: {
+          ...edge.node,
+          follows: followsLookup[edge.node.id]?.follows || 0,
+          views: viewsLookup[edge.node.id]?.views || 0,
+        },
+      }))
+    },
+  },
 })
 })

+ 16 - 2
src/api/client/transforms/index.ts

@@ -1,4 +1,18 @@
-export { ORION_FOLLOWS_QUERY_NAME, TransformOrionFollowsField } from './orionFollows'
-export { ORION_BATCHED_VIEWS_QUERY_NAME, TransformBatchedOrionViewsField } from './orionViews'
+export {
+  ORION_BATCHED_FOLLOWS_QUERY_NAME,
+  ORION_FOLLOWS_QUERY_NAME,
+  TransformBatchedOrionFollowsField,
+  TransformOrionFollowsField,
+} from './orionFollows'
+export {
+  ORION_BATCHED_VIEWS_QUERY_NAME,
+  ORION_VIEWS_QUERY_NAME,
+  TransformBatchedOrionViewsField,
+  TransformOrionViewsField,
+  ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
+  ORION_CHANNEL_VIEWS_QUERY_NAME,
+  TransformBatchedChannelOrionViewsField,
+  TransformOrionChannelViewsField,
+} from './orionViews'
 export { RemoveQueryNodeFollowsField } from './queryNodeFollows'
 export { RemoveQueryNodeFollowsField } from './queryNodeFollows'
 export { RemoveQueryNodeViewsField } from './queryNodeViews'
 export { RemoveQueryNodeViewsField } from './queryNodeViews'

+ 38 - 0
src/api/client/transforms/orionFollows.ts

@@ -74,3 +74,41 @@ export const TransformOrionFollowsField: Transform = {
     return { data }
     return { data }
   },
   },
 }
 }
+
+export const ORION_BATCHED_FOLLOWS_QUERY_NAME = 'batchedChannelFollows'
+
+export const TransformBatchedOrionFollowsField: Transform = {
+  transformRequest(request) {
+    request.document = {
+      ...request.document,
+      definitions: request.document.definitions.map((definition) => {
+        if (definition.kind === 'OperationDefinition') {
+          return {
+            ...definition,
+            selectionSet: {
+              ...definition.selectionSet,
+              selections: definition.selectionSet.selections.map((selection) => {
+                if (selection.kind === 'Field' && selection.name.value === ORION_BATCHED_FOLLOWS_QUERY_NAME) {
+                  return {
+                    ...selection,
+                    selectionSet: CHANNEL_INFO_SELECTION_SET,
+                  }
+                }
+                return selection
+              }),
+            },
+          }
+        }
+        return definition
+      }),
+    }
+
+    return request
+  },
+  transformResult(result) {
+    if (result.errors) {
+      throw new OrionError(result.errors)
+    }
+    return result
+  },
+}

+ 83 - 3
src/api/client/transforms/orionViews.ts

@@ -10,7 +10,7 @@ class OrionError extends Error {
   }
   }
 }
 }
 
 
-const VIDEO_INFO_SELECTION_SET: SelectionSetNode = {
+const INFO_SELECTION_SET: SelectionSetNode = {
   kind: 'SelectionSet',
   kind: 'SelectionSet',
   selections: [
   selections: [
     {
     {
@@ -47,7 +47,7 @@ export const TransformOrionViewsField: Transform = {
                 if (selection.kind === 'Field' && selection.name.value === ORION_VIEWS_QUERY_NAME) {
                 if (selection.kind === 'Field' && selection.name.value === ORION_VIEWS_QUERY_NAME) {
                   return {
                   return {
                     ...selection,
                     ...selection,
-                    selectionSet: VIDEO_INFO_SELECTION_SET,
+                    selectionSet: INFO_SELECTION_SET,
                   }
                   }
                 }
                 }
                 return selection
                 return selection
@@ -91,7 +91,87 @@ export const TransformBatchedOrionViewsField: Transform = {
                 if (selection.kind === 'Field' && selection.name.value === ORION_BATCHED_VIEWS_QUERY_NAME) {
                 if (selection.kind === 'Field' && selection.name.value === ORION_BATCHED_VIEWS_QUERY_NAME) {
                   return {
                   return {
                     ...selection,
                     ...selection,
-                    selectionSet: VIDEO_INFO_SELECTION_SET,
+                    selectionSet: INFO_SELECTION_SET,
+                  }
+                }
+                return selection
+              }),
+            },
+          }
+        }
+        return definition
+      }),
+    }
+
+    return request
+  },
+  transformResult(result) {
+    if (result.errors) {
+      throw new OrionError(result.errors)
+    }
+    return result
+  },
+}
+
+export const ORION_CHANNEL_VIEWS_QUERY_NAME = 'channelViews'
+export const TransformOrionChannelViewsField: Transform = {
+  transformRequest(request) {
+    request.document = {
+      ...request.document,
+      definitions: request.document.definitions.map((definition) => {
+        if (definition.kind === 'OperationDefinition') {
+          return {
+            ...definition,
+            selectionSet: {
+              ...definition.selectionSet,
+              selections: definition.selectionSet.selections.map((selection) => {
+                if (selection.kind === 'Field' && selection.name.value === ORION_CHANNEL_VIEWS_QUERY_NAME) {
+                  return {
+                    ...selection,
+                    selectionSet: INFO_SELECTION_SET,
+                  }
+                }
+                return selection
+              }),
+            },
+          }
+        }
+        return definition
+      }),
+    }
+
+    return request
+  },
+  transformResult(result) {
+    if (result.errors) {
+      throw new OrionError(result.errors)
+    }
+
+    const views = result?.data?.[ORION_CHANNEL_VIEWS_QUERY_NAME]?.views || 0
+    const data = {
+      channelViews: views,
+    }
+    return { data }
+  },
+}
+
+export const ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME = 'batchedChannelsViews'
+
+export const TransformBatchedChannelOrionViewsField: Transform = {
+  transformRequest(request) {
+    request.document = {
+      ...request.document,
+      definitions: request.document.definitions.map((definition) => {
+        if (definition.kind === 'OperationDefinition') {
+          return {
+            ...definition,
+            selectionSet: {
+              ...definition.selectionSet,
+              selections: definition.selectionSet.selections.map((selection) => {
+                if (selection.kind === 'Field' && selection.name.value === ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME) {
+                  return {
+                    ...selection,
+                    selectionSet: INFO_SELECTION_SET,
                   }
                   }
                 }
                 }
                 return selection
                 return selection

+ 24 - 0
src/api/client/transforms/queryNodeViews.ts

@@ -23,3 +23,27 @@ export const RemoveQueryNodeViewsField: Transform = {
     return request
     return request
   },
   },
 }
 }
+
+// remove views field from the query node video request
+export const RemoveQueryNodeChannelViewsField: Transform = {
+  transformRequest: (request) => {
+    request.document = {
+      ...request.document,
+      definitions: request.document.definitions.map((definition) => {
+        if (definition.kind === 'FragmentDefinition' && definition.name.value === 'AllChannelFields') {
+          return {
+            ...definition,
+            selectionSet: {
+              ...definition.selectionSet,
+              selections: definition.selectionSet.selections.filter((selection) => {
+                return selection.kind !== 'Field' || selection.name.value !== 'views'
+              }),
+            },
+          }
+        }
+        return definition
+      }),
+    }
+    return request
+  },
+}

+ 2 - 1
src/api/hooks/channel.ts

@@ -42,7 +42,7 @@ export const useChannel = (id: string, opts?: ChannelOpts) => {
 }
 }
 
 
 type VideoCountOpts = QueryHookOptions<GetVideoCountQuery>
 type VideoCountOpts = QueryHookOptions<GetVideoCountQuery>
-export const useChannelVideoCount = (channelId: string, opts?: VideoCountOpts) => {
+export const useChannelVideoCount = (channelId: string, createdAt_gte?: Date, opts?: VideoCountOpts) => {
   const { data, ...rest } = useGetVideoCountQuery({
   const { data, ...rest } = useGetVideoCountQuery({
     ...opts,
     ...opts,
     variables: {
     variables: {
@@ -52,6 +52,7 @@ export const useChannelVideoCount = (channelId: string, opts?: VideoCountOpts) =
         mediaAvailability_eq: AssetAvailability.Accepted,
         mediaAvailability_eq: AssetAvailability.Accepted,
         isPublic_eq: true,
         isPublic_eq: true,
         isCensored_eq: false,
         isCensored_eq: false,
+        createdAt_gte: createdAt_gte,
       },
       },
     },
     },
   })
   })

+ 3 - 0
src/api/queries/__generated__/baseTypes.generated.ts

@@ -115,6 +115,7 @@ export type Channel = {
   avatarPhotoUrls: Array<Scalars['String']>
   avatarPhotoUrls: Array<Scalars['String']>
   avatarPhotoAvailability: AssetAvailability
   avatarPhotoAvailability: AssetAvailability
   follows?: Maybe<Scalars['Int']>
   follows?: Maybe<Scalars['Int']>
+  views?: Maybe<Scalars['Int']>
 }
 }
 
 
 export type ChannelWhereInput = {
 export type ChannelWhereInput = {
@@ -311,6 +312,8 @@ export type QueryChannelViewsArgs = {
 }
 }
 
 
 export type QueryChannelsArgs = {
 export type QueryChannelsArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
   where?: Maybe<ChannelWhereInput>
   where?: Maybe<ChannelWhereInput>
 }
 }
 
 

+ 173 - 2
src/api/queries/__generated__/channels.generated.tsx

@@ -19,11 +19,13 @@ export type AllChannelFieldsFragment = {
   __typename?: 'Channel'
   __typename?: 'Channel'
   description?: Types.Maybe<string>
   description?: Types.Maybe<string>
   follows?: Types.Maybe<number>
   follows?: Types.Maybe<number>
+  views?: Types.Maybe<number>
   isPublic?: Types.Maybe<boolean>
   isPublic?: Types.Maybe<boolean>
   isCensored: boolean
   isCensored: boolean
   coverPhotoUrls: Array<string>
   coverPhotoUrls: Array<string>
   coverPhotoAvailability: Types.AssetAvailability
   coverPhotoAvailability: Types.AssetAvailability
   language?: Types.Maybe<{ __typename?: 'Language'; iso: string }>
   language?: Types.Maybe<{ __typename?: 'Language'; iso: string }>
+  ownerMember?: Types.Maybe<{ __typename?: 'Membership'; id: string; handle: string; avatarUri?: Types.Maybe<string> }>
   coverPhotoDataObject?: Types.Maybe<{ __typename?: 'DataObject' } & DataObjectFieldsFragment>
   coverPhotoDataObject?: Types.Maybe<{ __typename?: 'DataObject' } & DataObjectFieldsFragment>
 } & BasicChannelFieldsFragment
 } & BasicChannelFieldsFragment
 
 
@@ -55,6 +57,8 @@ export type GetVideoCountQuery = {
 }
 }
 
 
 export type GetChannelsQueryVariables = Types.Exact<{
 export type GetChannelsQueryVariables = Types.Exact<{
+  offset?: Types.Maybe<Types.Scalars['Int']>
+  limit?: Types.Maybe<Types.Scalars['Int']>
   where?: Types.Maybe<Types.ChannelWhereInput>
   where?: Types.Maybe<Types.ChannelWhereInput>
 }>
 }>
 
 
@@ -92,6 +96,33 @@ export type GetChannelFollowsQuery = {
   channelFollows?: Types.Maybe<{ __typename?: 'ChannelFollowsInfo'; id: string; follows: number }>
   channelFollows?: Types.Maybe<{ __typename?: 'ChannelFollowsInfo'; id: string; follows: number }>
 }
 }
 
 
+export type GetBatchedChannelFollowsQueryVariables = Types.Exact<{
+  channelIdList: Array<Types.Scalars['ID']> | Types.Scalars['ID']
+}>
+
+export type GetBatchedChannelFollowsQuery = {
+  __typename?: 'Query'
+  batchedChannelFollows: Array<Types.Maybe<{ __typename?: 'ChannelFollowsInfo'; id: string; follows: number }>>
+}
+
+export type GetChannelViewsQueryVariables = Types.Exact<{
+  channelId: Types.Scalars['ID']
+}>
+
+export type GetChannelViewsQuery = {
+  __typename?: 'Query'
+  channelViews?: Types.Maybe<{ __typename?: 'EntityViewsInfo'; id: string; views: number }>
+}
+
+export type GetBatchedChannelViewsQueryVariables = Types.Exact<{
+  channelIdList: Array<Types.Scalars['ID']> | Types.Scalars['ID']
+}>
+
+export type GetBatchedChannelViewsQuery = {
+  __typename?: 'Query'
+  batchedChannelsViews: Array<Types.Maybe<{ __typename?: 'EntityViewsInfo'; id: string; views: number }>>
+}
+
 export type FollowChannelMutationVariables = Types.Exact<{
 export type FollowChannelMutationVariables = Types.Exact<{
   channelId: Types.Scalars['ID']
   channelId: Types.Scalars['ID']
 }>
 }>
@@ -128,11 +159,17 @@ export const AllChannelFieldsFragmentDoc = gql`
     ...BasicChannelFields
     ...BasicChannelFields
     description
     description
     follows
     follows
+    views
     isPublic
     isPublic
     isCensored
     isCensored
     language {
     language {
       iso
       iso
     }
     }
+    ownerMember {
+      id
+      handle
+      avatarUri
+    }
     coverPhotoUrls
     coverPhotoUrls
     coverPhotoAvailability
     coverPhotoAvailability
     coverPhotoDataObject {
     coverPhotoDataObject {
@@ -254,8 +291,8 @@ export type GetVideoCountQueryHookResult = ReturnType<typeof useGetVideoCountQue
 export type GetVideoCountLazyQueryHookResult = ReturnType<typeof useGetVideoCountLazyQuery>
 export type GetVideoCountLazyQueryHookResult = ReturnType<typeof useGetVideoCountLazyQuery>
 export type GetVideoCountQueryResult = Apollo.QueryResult<GetVideoCountQuery, GetVideoCountQueryVariables>
 export type GetVideoCountQueryResult = Apollo.QueryResult<GetVideoCountQuery, GetVideoCountQueryVariables>
 export const GetChannelsDocument = gql`
 export const GetChannelsDocument = gql`
-  query GetChannels($where: ChannelWhereInput) {
-    channels(where: $where) {
+  query GetChannels($offset: Int, $limit: Int, $where: ChannelWhereInput) {
+    channels(offset: $offset, limit: $limit, where: $where) {
       ...AllChannelFields
       ...AllChannelFields
     }
     }
   }
   }
@@ -274,6 +311,8 @@ export const GetChannelsDocument = gql`
  * @example
  * @example
  * const { data, loading, error } = useGetChannelsQuery({
  * const { data, loading, error } = useGetChannelsQuery({
  *   variables: {
  *   variables: {
+ *      offset: // value for 'offset'
+ *      limit: // value for 'limit'
  *      where: // value for 'where'
  *      where: // value for 'where'
  *   },
  *   },
  * });
  * });
@@ -394,6 +433,138 @@ export function useGetChannelFollowsLazyQuery(
 export type GetChannelFollowsQueryHookResult = ReturnType<typeof useGetChannelFollowsQuery>
 export type GetChannelFollowsQueryHookResult = ReturnType<typeof useGetChannelFollowsQuery>
 export type GetChannelFollowsLazyQueryHookResult = ReturnType<typeof useGetChannelFollowsLazyQuery>
 export type GetChannelFollowsLazyQueryHookResult = ReturnType<typeof useGetChannelFollowsLazyQuery>
 export type GetChannelFollowsQueryResult = Apollo.QueryResult<GetChannelFollowsQuery, GetChannelFollowsQueryVariables>
 export type GetChannelFollowsQueryResult = Apollo.QueryResult<GetChannelFollowsQuery, GetChannelFollowsQueryVariables>
+export const GetBatchedChannelFollowsDocument = gql`
+  query GetBatchedChannelFollows($channelIdList: [ID!]!) {
+    batchedChannelFollows(channelIdList: $channelIdList) {
+      id
+      follows
+    }
+  }
+`
+
+/**
+ * __useGetBatchedChannelFollowsQuery__
+ *
+ * To run a query within a React component, call `useGetBatchedChannelFollowsQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetBatchedChannelFollowsQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetBatchedChannelFollowsQuery({
+ *   variables: {
+ *      channelIdList: // value for 'channelIdList'
+ *   },
+ * });
+ */
+export function useGetBatchedChannelFollowsQuery(
+  baseOptions: Apollo.QueryHookOptions<GetBatchedChannelFollowsQuery, GetBatchedChannelFollowsQueryVariables>
+) {
+  return Apollo.useQuery<GetBatchedChannelFollowsQuery, GetBatchedChannelFollowsQueryVariables>(
+    GetBatchedChannelFollowsDocument,
+    baseOptions
+  )
+}
+export function useGetBatchedChannelFollowsLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetBatchedChannelFollowsQuery, GetBatchedChannelFollowsQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetBatchedChannelFollowsQuery, GetBatchedChannelFollowsQueryVariables>(
+    GetBatchedChannelFollowsDocument,
+    baseOptions
+  )
+}
+export type GetBatchedChannelFollowsQueryHookResult = ReturnType<typeof useGetBatchedChannelFollowsQuery>
+export type GetBatchedChannelFollowsLazyQueryHookResult = ReturnType<typeof useGetBatchedChannelFollowsLazyQuery>
+export type GetBatchedChannelFollowsQueryResult = Apollo.QueryResult<
+  GetBatchedChannelFollowsQuery,
+  GetBatchedChannelFollowsQueryVariables
+>
+export const GetChannelViewsDocument = gql`
+  query GetChannelViews($channelId: ID!) {
+    channelViews(channelId: $channelId) {
+      id
+      views
+    }
+  }
+`
+
+/**
+ * __useGetChannelViewsQuery__
+ *
+ * To run a query within a React component, call `useGetChannelViewsQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetChannelViewsQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetChannelViewsQuery({
+ *   variables: {
+ *      channelId: // value for 'channelId'
+ *   },
+ * });
+ */
+export function useGetChannelViewsQuery(
+  baseOptions: Apollo.QueryHookOptions<GetChannelViewsQuery, GetChannelViewsQueryVariables>
+) {
+  return Apollo.useQuery<GetChannelViewsQuery, GetChannelViewsQueryVariables>(GetChannelViewsDocument, baseOptions)
+}
+export function useGetChannelViewsLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetChannelViewsQuery, GetChannelViewsQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetChannelViewsQuery, GetChannelViewsQueryVariables>(GetChannelViewsDocument, baseOptions)
+}
+export type GetChannelViewsQueryHookResult = ReturnType<typeof useGetChannelViewsQuery>
+export type GetChannelViewsLazyQueryHookResult = ReturnType<typeof useGetChannelViewsLazyQuery>
+export type GetChannelViewsQueryResult = Apollo.QueryResult<GetChannelViewsQuery, GetChannelViewsQueryVariables>
+export const GetBatchedChannelViewsDocument = gql`
+  query GetBatchedChannelViews($channelIdList: [ID!]!) {
+    batchedChannelsViews(channelIdList: $channelIdList) {
+      id
+      views
+    }
+  }
+`
+
+/**
+ * __useGetBatchedChannelViewsQuery__
+ *
+ * To run a query within a React component, call `useGetBatchedChannelViewsQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetBatchedChannelViewsQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetBatchedChannelViewsQuery({
+ *   variables: {
+ *      channelIdList: // value for 'channelIdList'
+ *   },
+ * });
+ */
+export function useGetBatchedChannelViewsQuery(
+  baseOptions: Apollo.QueryHookOptions<GetBatchedChannelViewsQuery, GetBatchedChannelViewsQueryVariables>
+) {
+  return Apollo.useQuery<GetBatchedChannelViewsQuery, GetBatchedChannelViewsQueryVariables>(
+    GetBatchedChannelViewsDocument,
+    baseOptions
+  )
+}
+export function useGetBatchedChannelViewsLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetBatchedChannelViewsQuery, GetBatchedChannelViewsQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetBatchedChannelViewsQuery, GetBatchedChannelViewsQueryVariables>(
+    GetBatchedChannelViewsDocument,
+    baseOptions
+  )
+}
+export type GetBatchedChannelViewsQueryHookResult = ReturnType<typeof useGetBatchedChannelViewsQuery>
+export type GetBatchedChannelViewsLazyQueryHookResult = ReturnType<typeof useGetBatchedChannelViewsLazyQuery>
+export type GetBatchedChannelViewsQueryResult = Apollo.QueryResult<
+  GetBatchedChannelViewsQuery,
+  GetBatchedChannelViewsQueryVariables
+>
 export const FollowChannelDocument = gql`
 export const FollowChannelDocument = gql`
   mutation FollowChannel($channelId: ID!) {
   mutation FollowChannel($channelId: ID!) {
     followChannel(channelId: $channelId) {
     followChannel(channelId: $channelId) {

+ 4 - 2
src/api/queries/__generated__/search.generated.tsx

@@ -11,6 +11,7 @@ export type SearchQueryVariables = Types.Exact<{
   text: Types.Scalars['String']
   text: Types.Scalars['String']
   whereVideo?: Types.Maybe<Types.VideoWhereInput>
   whereVideo?: Types.Maybe<Types.VideoWhereInput>
   whereChannel?: Types.Maybe<Types.ChannelWhereInput>
   whereChannel?: Types.Maybe<Types.ChannelWhereInput>
+  limit?: Types.Maybe<Types.Scalars['Int']>
 }>
 }>
 
 
 export type SearchQuery = {
 export type SearchQuery = {
@@ -22,8 +23,8 @@ export type SearchQuery = {
 }
 }
 
 
 export const SearchDocument = gql`
 export const SearchDocument = gql`
-  query Search($text: String!, $whereVideo: VideoWhereInput, $whereChannel: ChannelWhereInput) {
-    search(text: $text, whereVideo: $whereVideo, whereChannel: $whereChannel) {
+  query Search($text: String!, $whereVideo: VideoWhereInput, $whereChannel: ChannelWhereInput, $limit: Int) {
+    search(text: $text, whereVideo: $whereVideo, whereChannel: $whereChannel, limit: $limit) {
       item {
       item {
         ... on Video {
         ... on Video {
           ...VideoFields
           ...VideoFields
@@ -53,6 +54,7 @@ export const SearchDocument = gql`
  *      text: // value for 'text'
  *      text: // value for 'text'
  *      whereVideo: // value for 'whereVideo'
  *      whereVideo: // value for 'whereVideo'
  *      whereChannel: // value for 'whereChannel'
  *      whereChannel: // value for 'whereChannel'
+ *      limit: // value for 'limit'
  *   },
  *   },
  * });
  * });
  */
  */

+ 29 - 3
src/api/queries/channels.graphql

@@ -14,12 +14,17 @@ fragment AllChannelFields on Channel {
   ...BasicChannelFields
   ...BasicChannelFields
   description
   description
   follows
   follows
+  views
   isPublic
   isPublic
   isCensored
   isCensored
   language {
   language {
     iso
     iso
   }
   }
-
+  ownerMember {
+    id
+    handle
+    avatarUri
+  }
   coverPhotoUrls
   coverPhotoUrls
   coverPhotoAvailability
   coverPhotoAvailability
   coverPhotoDataObject {
   coverPhotoDataObject {
@@ -45,8 +50,8 @@ query GetVideoCount($where: VideoWhereInput) {
   }
   }
 }
 }
 
 
-query GetChannels($where: ChannelWhereInput) {
-  channels(where: $where) {
+query GetChannels($offset: Int, $limit: Int, $where: ChannelWhereInput) {
+  channels(offset: $offset, limit: $limit, where: $where) {
     ...AllChannelFields
     ...AllChannelFields
   }
   }
 }
 }
@@ -77,6 +82,27 @@ query GetChannelFollows($channelId: ID!) {
   }
   }
 }
 }
 
 
+query GetBatchedChannelFollows($channelIdList: [ID!]!) {
+  batchedChannelFollows(channelIdList: $channelIdList) {
+    id
+    follows
+  }
+}
+# modyfying this query name will need a sync-up in `src/api/client/resolvers.ts`
+query GetChannelViews($channelId: ID!) {
+  channelViews(channelId: $channelId) {
+    id
+    views
+  }
+}
+
+query GetBatchedChannelViews($channelIdList: [ID!]!) {
+  batchedChannelsViews(channelIdList: $channelIdList) {
+    id
+    views
+  }
+}
+
 mutation FollowChannel($channelId: ID!) {
 mutation FollowChannel($channelId: ID!) {
   followChannel(channelId: $channelId) {
   followChannel(channelId: $channelId) {
     id
     id

+ 2 - 2
src/api/queries/search.graphql

@@ -1,5 +1,5 @@
-query Search($text: String!, $whereVideo: VideoWhereInput, $whereChannel: ChannelWhereInput) {
-  search(text: $text, whereVideo: $whereVideo, whereChannel: $whereChannel) {
+query Search($text: String!, $whereVideo: VideoWhereInput, $whereChannel: ChannelWhereInput, $limit: Int) {
+  search(text: $text, whereVideo: $whereVideo, whereChannel: $whereChannel, limit: $limit) {
     item {
     item {
       ... on Video {
       ... on Video {
         ...VideoFields
         ...VideoFields

+ 2 - 1
src/api/schemas/extendedQueryNode.graphql

@@ -110,6 +110,7 @@ type Channel {
 
 
   # === extended from Orion ===
   # === extended from Orion ===
   follows: Int
   follows: Int
+  views: Int
 }
 }
 
 
 input ChannelWhereInput {
 input ChannelWhereInput {
@@ -266,7 +267,7 @@ type Query {
   channelByUniqueInput(where: ChannelWhereUniqueInput!): Channel
   channelByUniqueInput(where: ChannelWhereUniqueInput!): Channel
 
 
   # List all channels by given constraints
   # List all channels by given constraints
-  channels(where: ChannelWhereInput): [Channel!]!
+  channels(offset: Int, limit: Int, where: ChannelWhereInput): [Channel!]!
 
 
   # List all channel by given constraints
   # List all channel by given constraints
   channelsConnection(
   channelsConnection(

+ 5 - 5
src/components/ChannelPreview.tsx → src/components/ChannelCard.tsx

@@ -4,25 +4,25 @@ import { useChannel } from '@/api/hooks'
 import { useChannelVideoCount } from '@/api/hooks/channel'
 import { useChannelVideoCount } from '@/api/hooks/channel'
 import { absoluteRoutes } from '@/config/routes'
 import { absoluteRoutes } from '@/config/routes'
 import { AssetType, useAsset } from '@/providers'
 import { AssetType, useAsset } from '@/providers'
-import { ChannelPreviewBase } from '@/shared/components'
+import { ChannelCardBase } from '@/shared/components'
 
 
-type ChannelPreviewProps = {
+type ChannelCardProps = {
   id?: string
   id?: string
   className?: string
   className?: string
   onClick?: (e: React.MouseEvent<HTMLElement>) => void
   onClick?: (e: React.MouseEvent<HTMLElement>) => void
 }
 }
 
 
-export const ChannelPreview: React.FC<ChannelPreviewProps> = ({ id, className, onClick }) => {
+export const ChannelCard: React.FC<ChannelCardProps> = ({ id, className, onClick }) => {
   const { channel, loading } = useChannel(id ?? '', { fetchPolicy: 'cache-first', skip: !id })
   const { channel, loading } = useChannel(id ?? '', { fetchPolicy: 'cache-first', skip: !id })
   const { url } = useAsset({ entity: channel, assetType: AssetType.AVATAR })
   const { url } = useAsset({ entity: channel, assetType: AssetType.AVATAR })
-  const { videoCount } = useChannelVideoCount(id ?? '', {
+  const { videoCount } = useChannelVideoCount(id ?? '', undefined, {
     fetchPolicy: 'cache-first',
     fetchPolicy: 'cache-first',
     skip: !id,
     skip: !id,
   })
   })
   const isLoading = loading || id === undefined
   const isLoading = loading || id === undefined
 
 
   return (
   return (
-    <ChannelPreviewBase
+    <ChannelCardBase
       className={className}
       className={className}
       title={channel?.title}
       title={channel?.title}
       channelHref={id ? absoluteRoutes.viewer.channel(id) : undefined}
       channelHref={id ? absoluteRoutes.viewer.channel(id) : undefined}

+ 3 - 3
src/components/ChannelGallery.tsx

@@ -5,7 +5,7 @@ import { BasicChannelFieldsFragment } from '@/api/queries'
 import { Gallery } from '@/shared/components'
 import { Gallery } from '@/shared/components'
 import { sizes } from '@/shared/theme'
 import { sizes } from '@/shared/theme'
 
 
-import { ChannelPreview } from './ChannelPreview'
+import { ChannelCard } from './ChannelCard'
 
 
 type ChannelGalleryProps = {
 type ChannelGalleryProps = {
   title?: string
   title?: string
@@ -27,13 +27,13 @@ export const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, channels
   return (
   return (
     <Gallery title={title} itemWidth={220} exactWidth={true} paddingLeft={sizes(2, true)} paddingTop={sizes(2, true)}>
     <Gallery title={title} itemWidth={220} exactWidth={true} paddingLeft={sizes(2, true)} paddingTop={sizes(2, true)}>
       {[...channels, ...placeholderItems].map((channel, idx) => (
       {[...channels, ...placeholderItems].map((channel, idx) => (
-        <StyledChannelPreview key={idx} id={channel.id} onClick={createClickHandler(channel.id)} />
+        <StyledChannelCard key={idx} id={channel.id} onClick={createClickHandler(channel.id)} />
       ))}
       ))}
     </Gallery>
     </Gallery>
   )
   )
 }
 }
 
 
-const StyledChannelPreview = styled(ChannelPreview)`
+const StyledChannelCard = styled(ChannelCard)`
   + * {
   + * {
     margin-left: 16px;
     margin-left: 16px;
   }
   }

+ 3 - 3
src/components/ChannelGrid.tsx

@@ -4,9 +4,9 @@ import React from 'react'
 import { BasicChannelFieldsFragment } from '@/api/queries'
 import { BasicChannelFieldsFragment } from '@/api/queries'
 import { Grid } from '@/shared/components'
 import { Grid } from '@/shared/components'
 
 
-import { ChannelPreview } from './ChannelPreview'
+import { ChannelCard } from './ChannelCard'
 
 
-const StyledChannelPreview = styled(ChannelPreview)`
+const StyledChannelCard = styled(ChannelCard)`
   margin: 0 auto;
   margin: 0 auto;
 `
 `
 
 
@@ -25,7 +25,7 @@ export const ChannelGrid: React.FC<ChannelGridProps> = ({ channels, onChannelCli
   return (
   return (
     <Grid {...gridProps}>
     <Grid {...gridProps}>
       {channels.map(({ id }) => (
       {channels.map(({ id }) => (
-        <StyledChannelPreview key={id} id={id} onClick={() => handleClick(id)} />
+        <StyledChannelCard key={id} id={id} onClick={() => handleClick(id)} />
       ))}
       ))}
     </Grid>
     </Grid>
   )
   )

+ 2 - 2
src/components/ChannelLink/ChannelLink.style.ts

@@ -1,7 +1,7 @@
 import styled from '@emotion/styled'
 import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 import { Link } from 'react-router-dom'
 
 
-import { Placeholder } from '@/shared/components'
+import { SkeletonLoader } from '@/shared/components'
 import { colors, sizes, typography } from '@/shared/theme'
 import { colors, sizes, typography } from '@/shared/theme'
 
 
 type ContainerProps = {
 type ContainerProps = {
@@ -29,6 +29,6 @@ export const Handle = styled.span<HandleProps>`
   margin-left: ${({ withAvatar }) => (withAvatar ? sizes(2) : 0)};
   margin-left: ${({ withAvatar }) => (withAvatar ? sizes(2) : 0)};
 `
 `
 
 
-export const HandlePlaceholder = styled(Placeholder)<HandleProps>`
+export const HandleSkeletonLoader = styled(SkeletonLoader)<HandleProps>`
   margin-left: ${({ withAvatar }) => (withAvatar ? sizes(2) : 0)};
   margin-left: ${({ withAvatar }) => (withAvatar ? sizes(2) : 0)};
 `
 `

+ 2 - 2
src/components/ChannelLink/ChannelLink.tsx

@@ -7,7 +7,7 @@ import { AssetType, useAsset } from '@/providers'
 import { Avatar, AvatarSize } from '@/shared/components/Avatar'
 import { Avatar, AvatarSize } from '@/shared/components/Avatar'
 import { Logger } from '@/utils/logger'
 import { Logger } from '@/utils/logger'
 
 
-import { Container, Handle, HandlePlaceholder } from './ChannelLink.style'
+import { Container, Handle, HandleSkeletonLoader } from './ChannelLink.style'
 
 
 type ChannelLinkProps = {
 type ChannelLinkProps = {
   id?: string
   id?: string
@@ -50,7 +50,7 @@ export const ChannelLink: React.FC<ChannelLinkProps> = ({
         (displayedChannel ? (
         (displayedChannel ? (
           <Handle withAvatar={!hideAvatar}>{displayedChannel.title}</Handle>
           <Handle withAvatar={!hideAvatar}>{displayedChannel.title}</Handle>
         ) : (
         ) : (
-          <HandlePlaceholder withAvatar={!hideAvatar} height={16} width={150} />
+          <HandleSkeletonLoader withAvatar={!hideAvatar} height={16} width={150} />
         ))}
         ))}
     </Container>
     </Container>
   )
   )

+ 0 - 1
src/components/CoverVideo/index.ts

@@ -1 +0,0 @@
-export * from './CoverVideo'

+ 40 - 7
src/components/Dialogs/ActionDialog/ActionDialog.stories.tsx

@@ -14,12 +14,14 @@ export default {
   },
   },
   argTypes: {
   argTypes: {
     exitButton: { defaultValue: true },
     exitButton: { defaultValue: true },
-    primaryButtonText: { defaultValue: 'hello darkness' },
-    secondaryButtonText: { defaultValue: 'my old friend' },
+    primaryButtonText: { defaultValue: 'hello darkness', type: { name: 'string', required: false } },
+    secondaryButtonText: { defaultValue: 'my old friend', type: { name: 'string', required: false } },
     showDialog: { table: { disable: true } },
     showDialog: { table: { disable: true } },
     additionalActionsNode: { table: { disable: true } },
     additionalActionsNode: { table: { disable: true } },
     warning: { defaultValue: false },
     warning: { defaultValue: false },
     error: { defaultValue: false },
     error: { defaultValue: false },
+    primaryButton: { table: { disable: true } },
+    secondaryButton: { table: { disable: true } },
   },
   },
   decorators: [
   decorators: [
     (Story) => (
     (Story) => (
@@ -32,6 +34,8 @@ export default {
 
 
 type StoryProps = ActionDialogProps & {
 type StoryProps = ActionDialogProps & {
   showAdditionalAction?: boolean
   showAdditionalAction?: boolean
+  primaryButtonText?: string
+  secondaryButtonText?: string
 }
 }
 
 
 const additionalActionNode = (
 const additionalActionNode = (
@@ -47,23 +51,50 @@ const content = (
   </div>
   </div>
 )
 )
 
 
-const RegularTemplate: Story<StoryProps> = ({ showAdditionalAction, ...args }) => {
+const RegularTemplate: Story<StoryProps> = ({
+  showAdditionalAction,
+  primaryButtonText,
+  secondaryButtonText,
+  ...args
+}) => {
   return (
   return (
-    <ActionDialog {...args} showDialog={true} additionalActionsNode={showAdditionalAction && additionalActionNode} />
+    <ActionDialog
+      {...args}
+      primaryButton={{ text: primaryButtonText }}
+      secondaryButton={{ text: secondaryButtonText }}
+      showDialog={true}
+      additionalActionsNode={showAdditionalAction && additionalActionNode}
+    />
   )
   )
 }
 }
 export const Regular = RegularTemplate.bind({})
 export const Regular = RegularTemplate.bind({})
 
 
-const ContentTemplate: Story<StoryProps> = ({ showAdditionalAction, ...args }) => {
+const ContentTemplate: Story<StoryProps> = ({
+  showAdditionalAction,
+  primaryButtonText,
+  secondaryButtonText,
+  ...args
+}) => {
   return (
   return (
-    <ActionDialog {...args} showDialog={true} additionalActionsNode={showAdditionalAction && additionalActionNode}>
+    <ActionDialog
+      {...args}
+      primaryButton={{ text: primaryButtonText }}
+      secondaryButton={{ text: secondaryButtonText }}
+      showDialog={true}
+      additionalActionsNode={showAdditionalAction && additionalActionNode}
+    >
       {content}
       {content}
     </ActionDialog>
     </ActionDialog>
   )
   )
 }
 }
 export const WithContent = ContentTemplate.bind({})
 export const WithContent = ContentTemplate.bind({})
 
 
-const TransitionTemplate: Story<StoryProps> = ({ showAdditionalAction, ...args }) => {
+const TransitionTemplate: Story<StoryProps> = ({
+  showAdditionalAction,
+  primaryButtonText,
+  secondaryButtonText,
+  ...args
+}) => {
   const [showDialog, setShowDialog] = useState(false)
   const [showDialog, setShowDialog] = useState(false)
 
 
   return (
   return (
@@ -71,6 +102,8 @@ const TransitionTemplate: Story<StoryProps> = ({ showAdditionalAction, ...args }
       <Button onClick={() => setShowDialog(true)}>Open Dialog</Button>
       <Button onClick={() => setShowDialog(true)}>Open Dialog</Button>
       <ActionDialog
       <ActionDialog
         {...args}
         {...args}
+        primaryButton={{ text: primaryButtonText }}
+        secondaryButton={{ text: secondaryButtonText }}
         onExitClick={() => setShowDialog(false)}
         onExitClick={() => setShowDialog(false)}
         showDialog={showDialog}
         showDialog={showDialog}
         additionalActionsNode={showAdditionalAction && additionalActionNode}
         additionalActionsNode={showAdditionalAction && additionalActionNode}

+ 0 - 62
src/components/Dialogs/ActionDialog/ActionDialog.style.ts

@@ -1,14 +1,7 @@
-import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 import styled from '@emotion/styled'
 
 
-import { Button } from '@/shared/components'
 import { media, sizes } from '@/shared/theme'
 import { media, sizes } from '@/shared/theme'
 
 
-type ButtonProps = {
-  error?: boolean
-  warning?: boolean
-}
-
 export const ButtonsContainer = styled.div`
 export const ButtonsContainer = styled.div`
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
@@ -52,58 +45,3 @@ export const AdditionalActionsContainer = styled.div`
     margin-right: ${sizes(6)};
     margin-right: ${sizes(6)};
   }
   }
 `
 `
-
-const buttonColorsFromProps = ({ error, warning }: ButtonProps) => {
-  let color, bgColor, borderColor, bgActiveColor, borderActiveColor
-
-  if (warning) {
-    color = 'var(--warning-font-color)'
-    bgColor = 'var(--warning-bg-color)'
-    borderColor = 'var(--warning-bg-color)'
-    bgActiveColor = 'var(--warning-bg-active-color)'
-    borderActiveColor = 'var(--warning-border-active-color)'
-  }
-  if (error) {
-    color = 'var(--error-font-color)'
-    bgColor = 'var(--error-bg-color)'
-    borderColor = 'var(--error-bg-color)'
-    bgActiveColor = 'var(--error-bg-active-color)'
-    borderActiveColor = 'var(--error-border-active-color)'
-  }
-
-  const boxShadow = error || warning ? `inset 0px 0px 0px 1px ${borderActiveColor}` : 'none'
-
-  return css`
-    color: ${color};
-    background-color: ${bgColor};
-    border-color: ${borderColor};
-
-    &:hover {
-      color: ${color};
-      background-color: ${bgColor};
-      border-color: ${borderColor};
-      box-shadow: none;
-    }
-
-    &:active {
-      color: ${color};
-      background-color: ${bgActiveColor};
-      border-color: ${borderActiveColor};
-      box-shadow: ${boxShadow};
-    }
-  `
-}
-
-export const StyledPrimaryButton = styled(Button)<ButtonProps>`
-  --warning-bg-color: #f49525;
-  --warning-bg-active-color: #da7b0b;
-  --warning-border-active-color: #49290440;
-  --warning-font-color: #492904;
-
-  --error-bg-color: #e53333;
-  --error-bg-active-color: #cc1a1a;
-  --error-border-active-color: #44090966;
-  --error-font-color: #440909;
-
-  ${buttonColorsFromProps}
-`

+ 20 - 32
src/components/Dialogs/ActionDialog/ActionDialog.tsx

@@ -1,42 +1,35 @@
 import React from 'react'
 import React from 'react'
 
 
-import { Button } from '@/shared/components'
+import { Button, ButtonProps } from '@/shared/components'
 
 
-import {
-  ActionsContainer,
-  AdditionalActionsContainer,
-  ButtonsContainer,
-  StyledPrimaryButton,
-} from './ActionDialog.style'
+import { ActionsContainer, AdditionalActionsContainer, ButtonsContainer } from './ActionDialog.style'
 
 
 import { BaseDialog, BaseDialogProps } from '../BaseDialog'
 import { BaseDialog, BaseDialogProps } from '../BaseDialog'
 
 
+type DialogButtonProps = {
+  text?: string
+  disabled?: boolean
+  onClick?: (e: React.MouseEvent) => void
+} & Omit<ButtonProps, 'children'>
+
 export type ActionDialogProps = {
 export type ActionDialogProps = {
   additionalActionsNode?: React.ReactNode
   additionalActionsNode?: React.ReactNode
-  primaryButtonText?: string
-  secondaryButtonText?: string
-  primaryButtonDisabled?: boolean
-  secondaryButtonDisabled?: boolean
-  onPrimaryButtonClick?: (e: React.MouseEvent) => void
-  onSecondaryButtonClick?: (e: React.MouseEvent) => void
+  primaryButton?: DialogButtonProps
+  secondaryButton?: DialogButtonProps
   warning?: boolean
   warning?: boolean
   error?: boolean
   error?: boolean
 } & BaseDialogProps
 } & BaseDialogProps
 
 
 export const ActionDialog: React.FC<ActionDialogProps> = ({
 export const ActionDialog: React.FC<ActionDialogProps> = ({
   additionalActionsNode,
   additionalActionsNode,
-  primaryButtonText,
-  secondaryButtonText,
-  primaryButtonDisabled,
-  secondaryButtonDisabled,
-  onPrimaryButtonClick,
-  onSecondaryButtonClick,
+  primaryButton,
+  secondaryButton,
   warning,
   warning,
   error,
   error,
   children,
   children,
   ...baseDialogProps
   ...baseDialogProps
 }) => {
 }) => {
-  const hasAnyAction = additionalActionsNode || primaryButtonText || secondaryButtonText
+  const hasAnyAction = additionalActionsNode || primaryButton?.text || secondaryButton?.text
 
 
   return (
   return (
     <BaseDialog {...baseDialogProps}>
     <BaseDialog {...baseDialogProps}>
@@ -45,19 +38,14 @@ export const ActionDialog: React.FC<ActionDialogProps> = ({
         <ActionsContainer>
         <ActionsContainer>
           {additionalActionsNode && <AdditionalActionsContainer>{additionalActionsNode}</AdditionalActionsContainer>}
           {additionalActionsNode && <AdditionalActionsContainer>{additionalActionsNode}</AdditionalActionsContainer>}
           <ButtonsContainer>
           <ButtonsContainer>
-            {primaryButtonText && (
-              <StyledPrimaryButton
-                onClick={onPrimaryButtonClick}
-                warning={warning}
-                error={error}
-                disabled={primaryButtonDisabled}
-              >
-                {primaryButtonText}
-              </StyledPrimaryButton>
+            {primaryButton?.text && (
+              <Button variant={error ? 'destructive' : warning ? 'warning' : 'primary'} {...primaryButton}>
+                {primaryButton.text}
+              </Button>
             )}
             )}
-            {secondaryButtonText && (
-              <Button variant="secondary" onClick={onSecondaryButtonClick} disabled={secondaryButtonDisabled}>
-                {secondaryButtonText}
+            {secondaryButton?.text && (
+              <Button variant="secondary" {...secondaryButton}>
+                {secondaryButton.text}
               </Button>
               </Button>
             )}
             )}
           </ButtonsContainer>
           </ButtonsContainer>

+ 1 - 0
src/components/Dialogs/BaseDialog/BaseDialog.style.ts

@@ -15,6 +15,7 @@ export const DialogBackDrop = styled.div`
 `
 `
 
 
 export const StyledContainer = styled.div`
 export const StyledContainer = styled.div`
+  display: grid;
   --dialog-padding: ${sizes(4)};
   --dialog-padding: ${sizes(4)};
   ${media.small} {
   ${media.small} {
     --dialog-padding: ${sizes(6)};
     --dialog-padding: ${sizes(6)};

+ 4 - 4
src/components/Dialogs/ImageCropDialog/ImageCropDialog.stories.tsx

@@ -3,7 +3,7 @@ import { Meta, Story } from '@storybook/react'
 import React, { useRef, useState } from 'react'
 import React, { useRef, useState } from 'react'
 
 
 import { OverlayManagerProvider } from '@/providers'
 import { OverlayManagerProvider } from '@/providers'
-import { Avatar, Placeholder } from '@/shared/components'
+import { Avatar, SkeletonLoader } from '@/shared/components'
 import { AssetDimensions, ImageCropData } from '@/types/cropper'
 import { AssetDimensions, ImageCropData } from '@/types/cropper'
 
 
 import { ImageCropDialog, ImageCropDialogImperativeHandle, ImageCropDialogProps } from './ImageCropDialog'
 import { ImageCropDialog, ImageCropDialogImperativeHandle, ImageCropDialogProps } from './ImageCropDialog'
@@ -66,12 +66,12 @@ const RegularTemplate: Story<ImageCropDialogProps> = () => {
       {thumbnailImageUrl ? (
       {thumbnailImageUrl ? (
         <Image src={thumbnailImageUrl} onClick={() => thumbnailDialogRef.current?.open()} />
         <Image src={thumbnailImageUrl} onClick={() => thumbnailDialogRef.current?.open()} />
       ) : (
       ) : (
-        <ImagePlaceholder onClick={() => thumbnailDialogRef.current?.open()} />
+        <ImageSkeletonLoader onClick={() => thumbnailDialogRef.current?.open()} />
       )}
       )}
       {coverImageUrl ? (
       {coverImageUrl ? (
         <Image src={coverImageUrl} onClick={() => coverDialogRef.current?.open()} />
         <Image src={coverImageUrl} onClick={() => coverDialogRef.current?.open()} />
       ) : (
       ) : (
-        <ImagePlaceholder onClick={() => coverDialogRef.current?.open()} />
+        <ImageSkeletonLoader onClick={() => coverDialogRef.current?.open()} />
       )}
       )}
 
 
       <ImageCropDialog imageType="avatar" onConfirm={handleAvatarConfirm} ref={avatarDialogRef} />
       <ImageCropDialog imageType="avatar" onConfirm={handleAvatarConfirm} ref={avatarDialogRef} />
@@ -82,7 +82,7 @@ const RegularTemplate: Story<ImageCropDialogProps> = () => {
 }
 }
 export const Regular = RegularTemplate.bind({})
 export const Regular = RegularTemplate.bind({})
 
 
-const ImagePlaceholder = styled(Placeholder)`
+const ImageSkeletonLoader = styled(SkeletonLoader)`
   width: 600px;
   width: 600px;
   min-height: 200px;
   min-height: 200px;
   cursor: pointer;
   cursor: pointer;

+ 2 - 2
src/components/Dialogs/ImageCropDialog/ImageCropDialog.style.ts

@@ -1,7 +1,7 @@
 import { css } from '@emotion/react'
 import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 import styled from '@emotion/styled'
 
 
-import { Placeholder, Text } from '@/shared/components'
+import { SkeletonLoader, Text } from '@/shared/components'
 import { Slider } from '@/shared/components/Slider'
 import { Slider } from '@/shared/components/Slider'
 import { colors, sizes } from '@/shared/theme'
 import { colors, sizes } from '@/shared/theme'
 
 
@@ -51,7 +51,7 @@ const cropAreaSizeCss = css`
   height: 256px;
   height: 256px;
 `
 `
 
 
-export const CropPlaceholder = styled(Placeholder)`
+export const CropSkeletonLoader = styled(SkeletonLoader)`
   ${cropAreaSizeCss};
   ${cropAreaSizeCss};
 `
 `
 
 

+ 1 - 2
src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx

@@ -139,8 +139,7 @@ const ImageCropDialogComponent: React.ForwardRefRenderFunction<
       <HiddenInput type="file" accept="image/*" onChange={handleFileChange} ref={inputRef} />
       <HiddenInput type="file" accept="image/*" onChange={handleFileChange} ref={inputRef} />
       <StyledActionDialog
       <StyledActionDialog
         showDialog={showDialog && !!editedImageHref}
         showDialog={showDialog && !!editedImageHref}
-        primaryButtonText="Confirm"
-        onPrimaryButtonClick={handleConfirmClick}
+        primaryButton={{ text: 'Confirm', onClick: handleConfirmClick }}
         onExitClick={resetDialog}
         onExitClick={resetDialog}
         additionalActionsNode={zoomControlNode}
         additionalActionsNode={zoomControlNode}
       >
       >

+ 1 - 2
src/components/Dialogs/ImageCropDialog/cropper.ts

@@ -91,8 +91,7 @@ export const useCropper = ({ imageEl, imageType, cropData }: UseCropperOpts) =>
 
 
       setZoomRange([minZoom, maxZoom])
       setZoomRange([minZoom, maxZoom])
 
 
-      const middleZoom = minZoom + (maxZoom - minZoom) / 2
-      cropper.zoomTo(middleZoom)
+      cropper.zoomTo(minZoom)
 
 
       if (cropData) {
       if (cropData) {
         const { data, canvasData, cropBoxData, zoom } = cropData
         const { data, canvasData, cropBoxData, zoom } = cropData

+ 1 - 3
src/components/Dialogs/TransactionDialog/TransactionDialog.tsx

@@ -53,9 +53,7 @@ export const TransactionDialog: React.FC<TransactionDialogProps> = ({ status, on
   return (
   return (
     <ActionDialog
     <ActionDialog
       showDialog={!!stepDetails}
       showDialog={!!stepDetails}
-      onSecondaryButtonClick={onClose}
-      secondaryButtonText="Cancel"
-      secondaryButtonDisabled={!canCancel}
+      secondaryButton={{ text: 'Cancel', onClick: onClose, disabled: !canCancel }}
       exitButton={false}
       exitButton={false}
       {...actionDialogProps}
       {...actionDialogProps}
     >
     >

+ 3 - 3
src/components/InfiniteGrids/InfiniteChannelGrid.tsx

@@ -12,7 +12,7 @@ import { sizes } from '@/shared/theme'
 
 
 import { useInfiniteGrid } from './useInfiniteGrid'
 import { useInfiniteGrid } from './useInfiniteGrid'
 
 
-import { ChannelPreview } from '../ChannelPreview'
+import { ChannelCard } from '../ChannelCard'
 
 
 type InfiniteChannelGridProps = {
 type InfiniteChannelGridProps = {
   title?: string
   title?: string
@@ -67,7 +67,7 @@ export const InfiniteChannelGrid: React.FC<InfiniteChannelGridProps> = ({
     <>
     <>
       {/* we are reusing the components below by giving them the same keys */}
       {/* we are reusing the components below by giving them the same keys */}
       {[...displayedItems, ...placeholderItems].map((channel, idx) => (
       {[...displayedItems, ...placeholderItems].map((channel, idx) => (
-        <StyledChannelPreview key={idx} id={channel.id} />
+        <StyledChannelCard key={idx} id={channel.id} />
       ))}
       ))}
     </>
     </>
   )
   )
@@ -94,6 +94,6 @@ const previewCss = css`
   margin: 0 auto;
   margin: 0 auto;
 `
 `
 
 
-const StyledChannelPreview = styled(ChannelPreview)`
+const StyledChannelCard = styled(ChannelCard)`
   ${previewCss};
   ${previewCss};
 `
 `

+ 5 - 5
src/components/InfiniteGrids/InfiniteVideoGrid.tsx

@@ -8,12 +8,12 @@ import {
   GetVideosConnectionQueryVariables,
   GetVideosConnectionQueryVariables,
   VideoWhereInput,
   VideoWhereInput,
 } from '@/api/queries'
 } from '@/api/queries'
-import { Grid, Placeholder, Text } from '@/shared/components'
+import { Grid, SkeletonLoader, Text } from '@/shared/components'
 import { sizes } from '@/shared/theme'
 import { sizes } from '@/shared/theme'
 
 
 import { useInfiniteGrid } from './useInfiniteGrid'
 import { useInfiniteGrid } from './useInfiniteGrid'
 
 
-import { VideoPreview } from '../VideoPreview'
+import { VideoTile } from '../VideoTile'
 
 
 type InfiniteVideoGridProps = {
 type InfiniteVideoGridProps = {
   title?: string
   title?: string
@@ -141,7 +141,7 @@ export const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
   const gridContent = (
   const gridContent = (
     <>
     <>
       {[...displayedItems, ...placeholderItems]?.map((video, idx) => (
       {[...displayedItems, ...placeholderItems]?.map((video, idx) => (
-        <VideoPreview id={video.id} key={idx} showChannel={showChannel} />
+        <VideoTile id={video.id} key={idx} showChannel={showChannel} />
       ))}
       ))}
     </>
     </>
   )
   )
@@ -154,7 +154,7 @@ export const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
   // Right now we'll make the first request and then right after another one based on the resized columns
   // Right now we'll make the first request and then right after another one based on the resized columns
   return (
   return (
     <section className={className}>
     <section className={className}>
-      {title && (!ready ? <StyledPlaceholder height={23} width={250} /> : <Title variant="h5">{title}</Title>)}
+      {title && (!ready ? <StyledSkeletonLoader height={23} width={250} /> : <Title variant="h5">{title}</Title>)}
       <Grid onResize={(sizes) => setVideosPerRow(sizes.length)}>{gridContent}</Grid>
       <Grid onResize={(sizes) => setVideosPerRow(sizes.length)}>{gridContent}</Grid>
     </section>
     </section>
   )
   )
@@ -164,6 +164,6 @@ const Title = styled(Text)`
   margin-bottom: ${sizes(4)};
   margin-bottom: ${sizes(4)};
 `
 `
 
 
-const StyledPlaceholder = styled(Placeholder)`
+const StyledSkeletonLoader = styled(SkeletonLoader)`
   margin-bottom: ${sizes(4)};
   margin-bottom: ${sizes(4)};
 `
 `

+ 3 - 3
src/components/StudioContainer.tsx → src/components/LimitedWidthContainer.tsx

@@ -1,13 +1,13 @@
 import { css } from '@emotion/react'
 import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 import styled from '@emotion/styled'
 
 
-export const studioContainerStyle = css`
+export const limitedWidthContainerStyle = css`
   --max-inner-width: calc(1440px - var(--sidenav-collapsed-width) - calc(2 * var(--global-horizontal-padding)));
   --max-inner-width: calc(1440px - var(--sidenav-collapsed-width) - calc(2 * var(--global-horizontal-padding)));
 
 
   max-width: var(--max-inner-width);
   max-width: var(--max-inner-width);
   position: relative;
   position: relative;
   margin: 0 auto;
   margin: 0 auto;
 `
 `
-export const StudioContainer = styled.div`
-  ${studioContainerStyle}
+export const LimitedWidthContainer = styled.div`
+  ${limitedWidthContainerStyle}
 `
 `

+ 0 - 18
src/components/PlaceholderVideoGrid.tsx

@@ -1,18 +0,0 @@
-import React from 'react'
-
-import { Grid } from '@/shared/components'
-
-import { VideoPreview } from './VideoPreview'
-
-type PlaceholderVideoGridProps = {
-  videosCount?: number
-}
-export const PlaceholderVideoGrid: React.FC<PlaceholderVideoGridProps> = ({ videosCount = 10 }) => {
-  return (
-    <Grid>
-      {Array.from({ length: videosCount }).map((_, idx) => (
-        <VideoPreview key={idx} />
-      ))}
-    </Grid>
-  )
-}

+ 18 - 0
src/components/SkeletonLoaderVideoGrid.tsx

@@ -0,0 +1,18 @@
+import React from 'react'
+
+import { Grid } from '@/shared/components'
+
+import { VideoTile } from './VideoTile'
+
+type SkeletonLoaderVideoGridProps = {
+  videosCount?: number
+}
+export const SkeletonLoaderVideoGrid: React.FC<SkeletonLoaderVideoGridProps> = ({ videosCount = 10 }) => {
+  return (
+    <Grid>
+      {Array.from({ length: videosCount }).map((_, idx) => (
+        <VideoTile key={idx} />
+      ))}
+    </Grid>
+  )
+}

+ 2 - 2
src/components/Topbar/StudioTopbar/StudioTopbar.style.tsx

@@ -1,7 +1,7 @@
 import styled from '@emotion/styled'
 import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 import { Link } from 'react-router-dom'
 
 
-import { Avatar, Placeholder, Text } from '@/shared/components'
+import { Avatar, SkeletonLoader, Text } from '@/shared/components'
 import { colors, media, sizes, transitions, typography, zIndex } from '@/shared/theme'
 import { colors, media, sizes, transitions, typography, zIndex } from '@/shared/theme'
 
 
 import { TopbarBase } from '../TopbarBase'
 import { TopbarBase } from '../TopbarBase'
@@ -186,7 +186,7 @@ export const DrawerChannelsContainer = styled.div`
   overflow-x: hidden;
   overflow-x: hidden;
 `
 `
 
 
-export const AvatarPlaceholder = styled(Placeholder)`
+export const AvatarSkeletonLoader = styled(SkeletonLoader)`
   border-radius: 100%;
   border-radius: 100%;
   width: 42px;
   width: 42px;
   height: 42px;
   height: 42px;

+ 7 - 7
src/components/Topbar/StudioTopbar/StudioTopbar.tsx

@@ -6,12 +6,12 @@ import { BasicChannelFieldsFragment } from '@/api/queries'
 import { absoluteRoutes } from '@/config/routes'
 import { absoluteRoutes } from '@/config/routes'
 import { useDisplayDataLostWarning } from '@/hooks'
 import { useDisplayDataLostWarning } from '@/hooks'
 import { AssetType, useAsset, useEditVideoSheet, useUser } from '@/providers'
 import { AssetType, useAsset, useEditVideoSheet, useUser } from '@/providers'
-import { Button, ExpandButton, IconButton, Placeholder, Text } from '@/shared/components'
+import { Button, ExpandButton, IconButton, SkeletonLoader, Text } from '@/shared/components'
 import { SvgGlyphAddVideo, SvgGlyphCheck, SvgGlyphLogOut, SvgGlyphNewChannel } from '@/shared/icons'
 import { SvgGlyphAddVideo, SvgGlyphCheck, SvgGlyphLogOut, SvgGlyphNewChannel } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 import { transitions } from '@/shared/theme'
 
 
 import {
 import {
-  AvatarPlaceholder,
+  AvatarSkeletonLoader,
   ChannelInfoContainer,
   ChannelInfoContainer,
   DrawerChannelsContainer,
   DrawerChannelsContainer,
   DrawerContainer,
   DrawerContainer,
@@ -156,7 +156,7 @@ export const StudioTopbar: React.FC<StudioTopbarProps> = ({ hideChannelInfo, ful
               </IconButton>
               </IconButton>
             </CSSTransition>
             </CSSTransition>
             {activeMembershipLoading ? (
             {activeMembershipLoading ? (
-              <ChannelInfoPlaceholder />
+              <ChannelInfoSkeletonLoader />
             ) : activeMembership?.channels.length ? (
             ) : activeMembership?.channels.length ? (
               <ChannelInfo channel={currentChannel} memberName={activeMembership.handle} onClick={handleDrawerToggle} />
               <ChannelInfo channel={currentChannel} memberName={activeMembership.handle} onClick={handleDrawerToggle} />
             ) : (
             ) : (
@@ -275,13 +275,13 @@ const NavDrawer = React.forwardRef<HTMLDivElement, NavDrawerProps>(
 )
 )
 NavDrawer.displayName = 'NavDrawer'
 NavDrawer.displayName = 'NavDrawer'
 
 
-const ChannelInfoPlaceholder = () => {
+const ChannelInfoSkeletonLoader = () => {
   return (
   return (
     <ChannelInfoContainer>
     <ChannelInfoContainer>
-      <AvatarPlaceholder />
+      <AvatarSkeletonLoader />
       <TextContainer>
       <TextContainer>
-        <Placeholder width="100%" height="15px" bottomSpace="6px" />
-        <Placeholder width="70%" height="10px" />
+        <SkeletonLoader width="100%" height="15px" bottomSpace="6px" />
+        <SkeletonLoader width="70%" height="10px" />
       </TextContainer>
       </TextContainer>
     </ChannelInfoContainer>
     </ChannelInfoContainer>
   )
   )

+ 6 - 6
src/components/VideoGallery.tsx

@@ -2,11 +2,11 @@ import styled from '@emotion/styled'
 import React, { useCallback, useMemo, useState } from 'react'
 import React, { useCallback, useMemo, useState } from 'react'
 
 
 import { VideoFieldsFragment } from '@/api/queries'
 import { VideoFieldsFragment } from '@/api/queries'
-import { CAROUSEL_ARROW_HEIGHT, Gallery, MIN_VIDEO_PREVIEW_WIDTH } from '@/shared/components'
+import { CAROUSEL_ARROW_HEIGHT, Gallery, MIN_VIDEO_TILE_WIDTH } from '@/shared/components'
 import { breakpointsOfGrid } from '@/shared/components/Grid'
 import { breakpointsOfGrid } from '@/shared/components/Grid'
 import { sizes } from '@/shared/theme'
 import { sizes } from '@/shared/theme'
 
 
-import { VideoPreview } from './VideoPreview'
+import { VideoTile } from './VideoTile'
 
 
 interface VideoFieldsWithProgress extends VideoFieldsFragment {
 interface VideoFieldsWithProgress extends VideoFieldsFragment {
   progress?: number
   progress?: number
@@ -82,11 +82,11 @@ export const VideoGallery: React.FC<VideoGalleryProps> = ({
       paddingLeft={sizes(2, true)}
       paddingLeft={sizes(2, true)}
       paddingTop={sizes(2, true)}
       paddingTop={sizes(2, true)}
       responsive={breakpoints}
       responsive={breakpoints}
-      itemWidth={MIN_VIDEO_PREVIEW_WIDTH}
+      itemWidth={MIN_VIDEO_TILE_WIDTH}
       arrowPosition={arrowPosition}
       arrowPosition={arrowPosition}
     >
     >
       {[...videos, ...placeholderItems]?.map((video, idx) => (
       {[...videos, ...placeholderItems]?.map((video, idx) => (
-        <StyledVideoPreview
+        <StyledVideoTile
           id={video.id}
           id={video.id}
           progress={video?.progress}
           progress={video?.progress}
           key={idx}
           key={idx}
@@ -101,11 +101,11 @@ export const VideoGallery: React.FC<VideoGalleryProps> = ({
   )
   )
 }
 }
 
 
-const StyledVideoPreview = styled(VideoPreview)`
+const StyledVideoTile = styled(VideoTile)`
   & + & {
   & + & {
     margin-left: ${sizes(6)};
     margin-left: ${sizes(6)};
   }
   }
 
 
-  /* MIN_VIDEO_PREVIEW_WIDTH */
+  /* MIN_VIDEO_TILE_WIDTH */
   min-width: 300px;
   min-width: 300px;
 `
 `

+ 3 - 3
src/components/VideoGrid.tsx

@@ -4,9 +4,9 @@ import React from 'react'
 import { VideoFieldsFragment } from '@/api/queries'
 import { VideoFieldsFragment } from '@/api/queries'
 import { Grid } from '@/shared/components'
 import { Grid } from '@/shared/components'
 
 
-import { VideoPreview } from './VideoPreview'
+import { VideoTile } from './VideoTile'
 
 
-const StyledVideoPreview = styled(VideoPreview)`
+const StyledVideoTile = styled(VideoTile)`
   margin: 0 auto;
   margin: 0 auto;
   width: 100%;
   width: 100%;
 `
 `
@@ -21,7 +21,7 @@ export const VideoGrid: React.FC<VideoGridProps> = ({ videos, showChannel = true
   return (
   return (
     <Grid>
     <Grid>
       {videos.map((v, idx) => (
       {videos.map((v, idx) => (
-        <StyledVideoPreview
+        <StyledVideoTile
           key={idx}
           key={idx}
           id={v.id}
           id={v.id}
           showChannel={showChannel}
           showChannel={showChannel}

+ 4 - 20
src/components/CoverVideo/CoverVideo.style.ts → src/components/VideoHero/VideoHero.style.ts

@@ -1,8 +1,8 @@
-import { css, keyframes } from '@emotion/react'
+import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 import styled from '@emotion/styled'
-import { darken, fluidRange } from 'polished'
+import { fluidRange } from 'polished'
 
 
-import { Button, IconButton, Placeholder, Text } from '@/shared/components'
+import { Button, IconButton, SkeletonLoader, Text } from '@/shared/components'
 import { breakpoints, colors, media, sizes } from '@/shared/theme'
 import { breakpoints, colors, media, sizes } from '@/shared/theme'
 
 
 import { ChannelLink } from '../ChannelLink'
 import { ChannelLink } from '../ChannelLink'
@@ -69,22 +69,6 @@ export const PlayerContainer = styled.div`
   ${absoluteMediaCss};
   ${absoluteMediaCss};
 `
 `
 
 
-const pulse = keyframes`
-  0, 100% { 
-    background-color: ${colors.gray[800]}
-  }
-  50% {
-    background-color: ${darken(0.15, colors.gray[800])}
-  }
-`
-
-export const PlayerPlaceholder = styled.div`
-  ${absoluteMediaCss};
-
-  background-color: ${colors.gray[800]};
-  animation: ${pulse} 0.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
-`
-
 export const HorizontalGradientOverlay = styled.div`
 export const HorizontalGradientOverlay = styled.div`
   ${absoluteMediaCss};
   ${absoluteMediaCss};
 
 
@@ -214,7 +198,7 @@ export const Title = styled(Text)`
   }
   }
 `
 `
 
 
-export const TitlePlaceholder = styled(Placeholder)`
+export const TitleSkeletonLoader = styled(SkeletonLoader)`
   margin-bottom: ${sizes(4)};
   margin-bottom: ${sizes(4)};
 
 
   ${media.medium} {
   ${media.medium} {

+ 10 - 13
src/components/CoverVideo/CoverVideo.tsx → src/components/VideoHero/VideoHero.tsx

@@ -4,7 +4,7 @@ import { CSSTransition } from 'react-transition-group'
 
 
 import { absoluteRoutes } from '@/config/routes'
 import { absoluteRoutes } from '@/config/routes'
 import { AssetType, useAsset } from '@/providers'
 import { AssetType, useAsset } from '@/providers'
-import { Placeholder, VideoPlayer } from '@/shared/components'
+import { SkeletonLoader, VideoPlayer } from '@/shared/components'
 import { SvgPlayerPause, SvgPlayerPlay, SvgPlayerSoundOff, SvgPlayerSoundOn } from '@/shared/icons'
 import { SvgPlayerPause, SvgPlayerPlay, SvgPlayerSoundOff, SvgPlayerSoundOn } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 import { transitions } from '@/shared/theme'
 
 
@@ -18,20 +18,19 @@ import {
   MediaWrapper,
   MediaWrapper,
   PlayButton,
   PlayButton,
   PlayerContainer,
   PlayerContainer,
-  PlayerPlaceholder,
   SoundButton,
   SoundButton,
   StyledChannelLink,
   StyledChannelLink,
   Title,
   Title,
   TitleContainer,
   TitleContainer,
-  TitlePlaceholder,
+  TitleSkeletonLoader,
   VerticalGradientOverlay,
   VerticalGradientOverlay,
-} from './CoverVideo.style'
-import { useCoverVideo } from './coverVideoData'
+} from './VideoHero.style'
+import { useVideoHero } from './VideoHeroData'
 
 
 const VIDEO_PLAYBACK_DELAY = 1250
 const VIDEO_PLAYBACK_DELAY = 1250
 
 
-export const CoverVideo: React.FC = () => {
-  const coverVideo = useCoverVideo()
+export const VideoHero: React.FC = () => {
+  const coverVideo = useVideoHero()
 
 
   const [videoPlaying, setVideoPlaying] = useState(false)
   const [videoPlaying, setVideoPlaying] = useState(false)
   const [displayControls, setDisplayControls] = useState(false)
   const [displayControls, setDisplayControls] = useState(false)
@@ -61,7 +60,7 @@ export const CoverVideo: React.FC = () => {
       <MediaWrapper>
       <MediaWrapper>
         <Media>
         <Media>
           <PlayerContainer>
           <PlayerContainer>
-            {coverVideo ? (
+            {coverVideo && (
               <VideoPlayer
               <VideoPlayer
                 fluid
                 fluid
                 isInBackground
                 isInBackground
@@ -71,8 +70,6 @@ export const CoverVideo: React.FC = () => {
                 onDataLoaded={handlePlaybackDataLoaded}
                 onDataLoaded={handlePlaybackDataLoaded}
                 src={coverVideo?.coverCutMediaUrl}
                 src={coverVideo?.coverCutMediaUrl}
               />
               />
-            ) : (
-              <PlayerPlaceholder />
             )}
             )}
           </PlayerContainer>
           </PlayerContainer>
           {coverVideo && <HorizontalGradientOverlay />}
           {coverVideo && <HorizontalGradientOverlay />}
@@ -96,9 +93,9 @@ export const CoverVideo: React.FC = () => {
             </>
             </>
           ) : (
           ) : (
             <>
             <>
-              <TitlePlaceholder width={380} height={60} />
-              <Placeholder width={300} height={20} bottomSpace={4} />
-              <Placeholder width={200} height={20} />
+              <TitleSkeletonLoader width={380} height={60} />
+              <SkeletonLoader width={300} height={20} bottomSpace={4} />
+              <SkeletonLoader width={200} height={20} />
             </>
             </>
           )}
           )}
         </TitleContainer>
         </TitleContainer>

+ 3 - 3
src/components/CoverVideo/coverVideoData.ts → src/components/VideoHero/VideoHeroData.ts

@@ -6,7 +6,7 @@ import { VideoFieldsFragment } from '@/api/queries'
 import { COVER_VIDEO_INFO_URL } from '@/config/urls'
 import { COVER_VIDEO_INFO_URL } from '@/config/urls'
 import { Logger } from '@/utils/logger'
 import { Logger } from '@/utils/logger'
 
 
-import backupCoverVideoInfo from './backupCoverVideoInfo.json'
+import backupVideoHeroInfo from './backupVideoHeroInfo.json'
 
 
 type RawCoverInfo = {
 type RawCoverInfo = {
   videoId: string
   videoId: string
@@ -21,7 +21,7 @@ type CoverInfo =
     })
     })
   | null
   | null
 
 
-export const useCoverVideo = (): CoverInfo => {
+export const useVideoHero = (): CoverInfo => {
   const [fetchedCoverInfo, setFetchedCoverInfo] = useState<RawCoverInfo | null>(null)
   const [fetchedCoverInfo, setFetchedCoverInfo] = useState<RawCoverInfo | null>(null)
   const { video, error } = useVideo(fetchedCoverInfo?.videoId || '', { skip: !fetchedCoverInfo?.videoId })
   const { video, error } = useVideo(fetchedCoverInfo?.videoId || '', { skip: !fetchedCoverInfo?.videoId })
 
 
@@ -36,7 +36,7 @@ export const useCoverVideo = (): CoverInfo => {
         setFetchedCoverInfo(response.data)
         setFetchedCoverInfo(response.data)
       } catch (e) {
       } catch (e) {
         Logger.error(`Failed to fetch cover info from ${COVER_VIDEO_INFO_URL}. Using backup`, e)
         Logger.error(`Failed to fetch cover info from ${COVER_VIDEO_INFO_URL}. Using backup`, e)
-        setFetchedCoverInfo(backupCoverVideoInfo)
+        setFetchedCoverInfo(backupVideoHeroInfo)
       }
       }
     }
     }
 
 

+ 0 - 0
src/components/CoverVideo/backupCoverVideoInfo.json → src/components/VideoHero/backupVideoHeroInfo.json


+ 1 - 0
src/components/VideoHero/index.ts

@@ -0,0 +1 @@
+export * from './VideoHero'

+ 14 - 19
src/components/VideoPreview.tsx → src/components/VideoTile.tsx

@@ -5,21 +5,21 @@ import { AssetAvailability } from '@/api/queries'
 import { absoluteRoutes } from '@/config/routes'
 import { absoluteRoutes } from '@/config/routes'
 import { AssetType, singleDraftSelector, useAsset, useDraftStore } from '@/providers'
 import { AssetType, singleDraftSelector, useAsset, useDraftStore } from '@/providers'
 import {
 import {
-  VideoPreviewBase,
-  VideoPreviewBaseMetaProps,
-  VideoPreviewBaseProps,
-  VideoPreviewPublisherProps,
-} from '@/shared/components/VideoPreviewBase/VideoPreviewBase'
+  VideoTileBase,
+  VideoTileBaseMetaProps,
+  VideoTileBaseProps,
+  VideoTilePublisherProps,
+} from '@/shared/components/VideoTileBase/VideoTileBase'
 import { copyToClipboard, openInNewTab } from '@/utils/browser'
 import { copyToClipboard, openInNewTab } from '@/utils/browser'
 import { Logger } from '@/utils/logger'
 import { Logger } from '@/utils/logger'
 
 
-export type VideoPreviewProps = {
+export type VideoTileProps = {
   id?: string
   id?: string
   onNotFound?: () => void
   onNotFound?: () => void
-} & VideoPreviewBaseMetaProps &
-  Pick<VideoPreviewBaseProps, 'progress' | 'className'>
+} & VideoTileBaseMetaProps &
+  Pick<VideoTileBaseProps, 'progress' | 'className'>
 
 
-export const VideoPreview: React.FC<VideoPreviewProps> = ({ id, onNotFound, ...metaProps }) => {
+export const VideoTile: React.FC<VideoTileProps> = ({ id, onNotFound, ...metaProps }) => {
   const { video, loading, videoHref } = useVideoSharedLogic({ id, isDraft: false, onNotFound })
   const { video, loading, videoHref } = useVideoSharedLogic({ id, isDraft: false, onNotFound })
   const { url: thumbnailPhotoUrl } = useAsset({
   const { url: thumbnailPhotoUrl } = useAsset({
     entity: video,
     entity: video,
@@ -31,7 +31,7 @@ export const VideoPreview: React.FC<VideoPreviewProps> = ({ id, onNotFound, ...m
   })
   })
 
 
   return (
   return (
-    <VideoPreviewBase
+    <VideoTileBase
       publisherMode={false}
       publisherMode={false}
       title={video?.title}
       title={video?.title}
       channelTitle={video?.channel.title}
       channelTitle={video?.channel.title}
@@ -49,14 +49,9 @@ export const VideoPreview: React.FC<VideoPreviewProps> = ({ id, onNotFound, ...m
   )
   )
 }
 }
 
 
-export type VideoPreviewWPublisherProps = VideoPreviewProps &
-  Omit<VideoPreviewPublisherProps, 'publisherMode' | 'videoPublishState'>
-export const VideoPreviewPublisher: React.FC<VideoPreviewWPublisherProps> = ({
-  id,
-  isDraft,
-  onNotFound,
-  ...metaProps
-}) => {
+export type VideoTileWPublisherProps = VideoTileProps &
+  Omit<VideoTilePublisherProps, 'publisherMode' | 'videoPublishState'>
+export const VideoTilePublisher: React.FC<VideoTileWPublisherProps> = ({ id, isDraft, onNotFound, ...metaProps }) => {
   const { video, loading, videoHref } = useVideoSharedLogic({ id, isDraft, onNotFound })
   const { video, loading, videoHref } = useVideoSharedLogic({ id, isDraft, onNotFound })
   const draft = useDraftStore(singleDraftSelector(id ?? ''))
   const draft = useDraftStore(singleDraftSelector(id ?? ''))
   const { url: thumbnailPhotoUrl } = useAsset({
   const { url: thumbnailPhotoUrl } = useAsset({
@@ -71,7 +66,7 @@ export const VideoPreviewPublisher: React.FC<VideoPreviewWPublisherProps> = ({
   const hasThumbnailUploadFailed = video?.thumbnailPhotoAvailability === AssetAvailability.Pending
   const hasThumbnailUploadFailed = video?.thumbnailPhotoAvailability === AssetAvailability.Pending
 
 
   return (
   return (
-    <VideoPreviewBase
+    <VideoTileBase
       publisherMode
       publisherMode
       title={isDraft ? draft?.title : video?.title}
       title={isDraft ? draft?.title : video?.title}
       channelTitle={video?.channel.title}
       channelTitle={video?.channel.title}

+ 5 - 5
src/components/index.ts

@@ -1,12 +1,12 @@
 export * from './VideoGallery'
 export * from './VideoGallery'
-export * from './CoverVideo'
+export * from './VideoHero'
 export * from './ChannelGallery'
 export * from './ChannelGallery'
 export * from './Topbar/ViewerTopbar'
 export * from './Topbar/ViewerTopbar'
 export * from './Topbar/StudioTopbar'
 export * from './Topbar/StudioTopbar'
 export * from './VideoGrid'
 export * from './VideoGrid'
-export * from './PlaceholderVideoGrid'
-export * from './VideoPreview'
-export * from './ChannelPreview'
+export * from './SkeletonLoaderVideoGrid'
+export * from './VideoTile'
+export * from './ChannelCard'
 export * from './ChannelGrid'
 export * from './ChannelGrid'
 export { ErrorFallback as ViewErrorFallback } from './ViewErrorFallback'
 export { ErrorFallback as ViewErrorFallback } from './ViewErrorFallback'
 export * from './ErrorFallback'
 export * from './ErrorFallback'
@@ -19,7 +19,7 @@ export * from './InterruptedVideosGallery'
 export * from './ViewWrapper'
 export * from './ViewWrapper'
 export * from './Portal'
 export * from './Portal'
 export * from './Dialogs'
 export * from './Dialogs'
-export * from './StudioContainer'
+export * from './LimitedWidthContainer'
 export * from './Topbar'
 export * from './Topbar'
 export * from './NoConnectionIndicator'
 export * from './NoConnectionIndicator'
 export * from './SignInSteps'
 export * from './SignInSteps'

+ 6 - 0
src/config/sorting.ts

@@ -0,0 +1,6 @@
+import { VideoOrderByInput } from '@/api/queries'
+
+export const SORT_OPTIONS = [
+  { name: 'Newest first', value: VideoOrderByInput.CreatedAtDesc },
+  { name: 'Oldest first', value: VideoOrderByInput.CreatedAtAsc },
+]

+ 11 - 7
src/hooks/useDeleteVideo.tsx

@@ -18,17 +18,21 @@ export const useDeleteVideo = () => {
       exitButton: false,
       exitButton: false,
       description:
       description:
         'You will not be able to undo this. Deletion requires a blockchain transaction to complete. Currently there is no way to remove uploaded video assets.',
         'You will not be able to undo this. Deletion requires a blockchain transaction to complete. Currently there is no way to remove uploaded video assets.',
-      onPrimaryButtonClick: () => {
-        confirmDeleteVideo(videoId, () => onDeleteVideo?.())
-        closeDeleteVideoDialog()
+      primaryButton: {
+        text: 'Delete video',
+        onClick: () => {
+          confirmDeleteVideo(videoId, () => onDeleteVideo?.())
+          closeDeleteVideoDialog()
+        },
       },
       },
-      onSecondaryButtonClick: () => {
-        closeDeleteVideoDialog()
+      secondaryButton: {
+        text: 'Cancel',
+        onClick: () => {
+          closeDeleteVideoDialog()
+        },
       },
       },
       error: true,
       error: true,
       variant: 'warning',
       variant: 'warning',
-      primaryButtonText: 'Delete video',
-      secondaryButtonText: 'Cancel',
     })
     })
   }
   }
 
 

+ 12 - 8
src/hooks/useDisplayDataLostWarning.tsx

@@ -17,15 +17,19 @@ export const useDisplayDataLostWarning = () => {
       title: "Drafts' video & image data will be lost",
       title: "Drafts' video & image data will be lost",
       description:
       description:
         "Drafts' assets aren't stored permanently. If you proceed, you will need to reselect the files again.",
         "Drafts' assets aren't stored permanently. If you proceed, you will need to reselect the files again.",
-      primaryButtonText: 'Proceed',
-      secondaryButtonText: 'Cancel',
-      onPrimaryButtonClick: () => {
-        onConfirm?.()
-        closeDialog()
+      primaryButton: {
+        text: 'Proceed',
+        onClick: () => {
+          onConfirm?.()
+          closeDialog()
+        },
       },
       },
-      onSecondaryButtonClick: () => {
-        cancelDialog(onCancel)
-        closeDialog()
+      secondaryButton: {
+        text: 'Cancel',
+        onClick: () => {
+          cancelDialog(onCancel)
+          closeDialog()
+        },
       },
       },
       onExitClick: () => {
       onExitClick: () => {
         cancelDialog(onCancel)
         cancelDialog(onCancel)

+ 2 - 2
src/mocking/accessors/filtering.ts

@@ -80,11 +80,11 @@ const createPredicate = (key: string, value: any, testData: GenericData): Predic
 
 
 export const genericSort = <TData extends GenericData>(data: TData[], variables: SortingArgs): TData[] => {
 export const genericSort = <TData extends GenericData>(data: TData[], variables: SortingArgs): TData[] => {
   const { orderBy } = variables
   const { orderBy } = variables
-  if (!orderBy) {
+  if (!orderBy?.length) {
     return data
     return data
   }
   }
 
 
-  const [field, direction] = orderBy.split('_')
+  const [field, direction] = orderBy[0].split('_')
   if (!field || !direction) {
   if (!field || !direction) {
     Logger.warn(`error parsing orderBy: "${orderBy}"`)
     Logger.warn(`error parsing orderBy: "${orderBy}"`)
     return data
     return data

+ 15 - 0
src/mocking/accessors/orion.ts

@@ -1,4 +1,6 @@
 import {
 import {
+  GetBatchedChannelFollowsQuery,
+  GetBatchedChannelFollowsQueryVariables,
   GetBatchedVideoViewsQuery,
   GetBatchedVideoViewsQuery,
   GetBatchedVideoViewsQueryVariables,
   GetBatchedVideoViewsQueryVariables,
   GetChannelFollowsQuery,
   GetChannelFollowsQuery,
@@ -54,3 +56,16 @@ export const createChannelFollowsAccessor = (store: MocksStore) => (
     follows,
     follows,
   }
   }
 }
 }
+
+export const createBatchedChannelFollowsAccessor = (store: MocksStore) => (
+  variables: GetBatchedChannelFollowsQueryVariables
+): GetBatchedChannelFollowsQuery['batchedChannelFollows'] => {
+  const { channelIdList } = variables
+
+  const batchedChannelFollows = store.batchedChannelFollows.filter((follow) => channelIdList.includes(follow.id))
+  if (!batchedChannelFollows.length) {
+    return []
+  }
+
+  return batchedChannelFollows
+}

+ 4 - 4
src/mocking/data/mockChannels.ts

@@ -2,8 +2,8 @@ import { AllChannelFieldsFragment, AssetAvailability } from '@/api/queries'
 import { languages } from '@/config/languages'
 import { languages } from '@/config/languages'
 
 
 import { channelAvatarSources, channelPosterSources } from './mockImages'
 import { channelAvatarSources, channelPosterSources } from './mockImages'
+import rawVideoHero from './raw/VideoHero.json'
 import rawChannels from './raw/channels.json'
 import rawChannels from './raw/channels.json'
-import rawCoverVideo from './raw/coverVideo.json'
 
 
 export type MockChannel = AllChannelFieldsFragment
 export type MockChannel = AllChannelFieldsFragment
 
 
@@ -23,10 +23,10 @@ export const regularMockChannels: MockChannel[] = rawChannels.map((rawChannel, i
 }))
 }))
 
 
 export const coverMockChannel: MockChannel = {
 export const coverMockChannel: MockChannel = {
-  ...rawCoverVideo.channel,
+  ...rawVideoHero.channel,
   __typename: 'Channel',
   __typename: 'Channel',
-  createdAt: new Date(rawCoverVideo.channel.createdAt),
-  avatarPhotoUrls: [rawCoverVideo.channel.avatarPhotoUrl],
+  createdAt: new Date(rawVideoHero.channel.createdAt),
+  avatarPhotoUrls: [rawVideoHero.channel.avatarPhotoUrl],
   coverPhotoUrls: [],
   coverPhotoUrls: [],
   avatarPhotoAvailability: AssetAvailability.Accepted,
   avatarPhotoAvailability: AssetAvailability.Accepted,
   coverPhotoAvailability: AssetAvailability.Invalid,
   coverPhotoAvailability: AssetAvailability.Invalid,

+ 8 - 8
src/mocking/data/mockVideos.ts

@@ -5,7 +5,7 @@ import mockChannels, { coverMockChannel } from './mockChannels'
 import { thumbnailSources } from './mockImages'
 import { thumbnailSources } from './mockImages'
 import mockLicenses from './mockLicenses'
 import mockLicenses from './mockLicenses'
 import mockVideosMedia from './mockVideosMedia'
 import mockVideosMedia from './mockVideosMedia'
-import rawCoverVideo from './raw/coverVideo.json'
+import rawVideoHero from './raw/VideoHero.json'
 import rawVideos from './raw/videos.json'
 import rawVideos from './raw/videos.json'
 
 
 export type MockVideo = VideoFieldsFragment
 export type MockVideo = VideoFieldsFragment
@@ -33,16 +33,16 @@ const regularMockVideos: MockVideo[] = rawVideos.map((rawVideo, idx) => {
 })
 })
 
 
 const coverMockVideo: MockVideo = {
 const coverMockVideo: MockVideo = {
-  ...rawCoverVideo.video,
-  createdAt: new Date(rawCoverVideo.video.createdAt),
+  ...rawVideoHero.video,
+  createdAt: new Date(rawVideoHero.video.createdAt),
   channel: coverMockChannel,
   channel: coverMockChannel,
-  license: rawCoverVideo.license,
+  license: rawVideoHero.license,
   mediaAvailability: AssetAvailability.Accepted,
   mediaAvailability: AssetAvailability.Accepted,
-  mediaUrls: [rawCoverVideo.video.mediaUrl],
-  thumbnailPhotoUrls: [rawCoverVideo.video.thumbnailPhotoUrl],
+  mediaUrls: [rawVideoHero.video.mediaUrl],
+  thumbnailPhotoUrls: [rawVideoHero.video.thumbnailPhotoUrl],
   thumbnailPhotoAvailability: AssetAvailability.Accepted,
   thumbnailPhotoAvailability: AssetAvailability.Accepted,
-  mediaMetadata: rawCoverVideo.mediaMetadata,
-  duration: rawCoverVideo.mediaMetadata.duration,
+  mediaMetadata: rawVideoHero.mediaMetadata,
+  duration: rawVideoHero.mediaMetadata.duration,
   category: mockCategories[0],
   category: mockCategories[0],
   isPublic: Boolean(Math.round(Math.random())),
   isPublic: Boolean(Math.round(Math.random())),
   isCensored: Boolean(Math.round(Math.random())),
   isCensored: Boolean(Math.round(Math.random())),

+ 0 - 0
src/mocking/data/raw/coverVideo.json → src/mocking/data/raw/VideoHero.json


+ 9 - 0
src/mocking/handlers.ts

@@ -4,6 +4,9 @@ import {
   GetBasicChannelDocument,
   GetBasicChannelDocument,
   GetBasicChannelQuery,
   GetBasicChannelQuery,
   GetBasicChannelQueryVariables,
   GetBasicChannelQueryVariables,
+  GetBatchedChannelFollowsDocument,
+  GetBatchedChannelFollowsQuery,
+  GetBatchedChannelFollowsQueryVariables,
   GetBatchedVideoViewsDocument,
   GetBatchedVideoViewsDocument,
   GetBatchedVideoViewsQuery,
   GetBatchedVideoViewsQuery,
   GetBatchedVideoViewsQueryVariables,
   GetBatchedVideoViewsQueryVariables,
@@ -50,6 +53,7 @@ import { ORION_GRAPHQL_URL, QUERY_NODE_GRAPHQL_URL } from '@/config/urls'
 import { mockCategories, mockChannels, mockMemberships, mockVideos, mockWorkers } from '@/mocking/data'
 import { mockCategories, mockChannels, mockMemberships, mockVideos, mockWorkers } from '@/mocking/data'
 
 
 import {
 import {
+  createBatchedChannelFollowsAccessor,
   createBatchedVideoViewsAccessor,
   createBatchedVideoViewsAccessor,
   createChannelFollowsAccessor,
   createChannelFollowsAccessor,
   createCursorPaginationAccessor,
   createCursorPaginationAccessor,
@@ -150,6 +154,11 @@ const orionHandlers = [
     GetVideoViewsDocument,
     GetVideoViewsDocument,
     createVideoViewsAccessor(store)
     createVideoViewsAccessor(store)
   ),
   ),
+  createQueryHandler<GetBatchedChannelFollowsQuery, GetBatchedChannelFollowsQueryVariables>(
+    orion,
+    GetBatchedChannelFollowsDocument,
+    createBatchedChannelFollowsAccessor(store)
+  ),
   createQueryHandler<GetChannelFollowsQuery, GetChannelFollowsQueryVariables>(
   createQueryHandler<GetChannelFollowsQuery, GetChannelFollowsQueryVariables>(
     orion,
     orion,
     GetChannelFollowsDocument,
     GetChannelFollowsDocument,

+ 5 - 0
src/mocking/store.ts

@@ -5,6 +5,7 @@ export const createStore = (data: VideosChannelsData): MocksStore => {
     videoViews: {},
     videoViews: {},
     channelFollows: {},
     channelFollows: {},
     batchedVideoViews: [],
     batchedVideoViews: [],
+    batchedChannelFollows: [],
   }
   }
 
 
   const { videos, channels } = data
   const { videos, channels } = data
@@ -21,6 +22,10 @@ export const createStore = (data: VideosChannelsData): MocksStore => {
     if (c.follows) {
     if (c.follows) {
       store.channelFollows[c.id] = c.follows
       store.channelFollows[c.id] = c.follows
     }
     }
+    store.batchedChannelFollows.push({
+      id: c.id,
+      follows: c.follows || 0,
+    })
   })
   })
 
 
   return store
   return store

+ 5 - 1
src/mocking/types.ts

@@ -12,7 +12,7 @@ export type FilteringArgs<TWhere = VideoWhereInput | VideoWhereUniqueInput | Mem
   where?: TWhere | null
   where?: TWhere | null
 }
 }
 export type SortingArgs = {
 export type SortingArgs = {
-  orderBy?: string
+  orderBy?: [string]
 }
 }
 export type GenericData = Record<string, unknown>
 export type GenericData = Record<string, unknown>
 export type CountData = {
 export type CountData = {
@@ -36,6 +36,10 @@ export type MocksStore = {
     id: string
     id: string
   }[]
   }[]
   channelFollows: Record<string, number>
   channelFollows: Record<string, number>
+  batchedChannelFollows: {
+    follows: number
+    id: string
+  }[]
 }
 }
 export type Link = ReturnType<typeof graphql.link>
 export type Link = ReturnType<typeof graphql.link>
 export type BaseDataQuery<TQueryData = unknown> = {
 export type BaseDataQuery<TQueryData = unknown> = {

+ 9 - 5
src/providers/transactionManager/useTransaction.ts

@@ -30,9 +30,11 @@ export const useTransaction = (): HandleTransactionFn => {
     title: 'Something went wrong...',
     title: 'Something went wrong...',
     description:
     description:
       'Some unexpected error was encountered. If this persists, our Discord community may be a good place to find some help.',
       'Some unexpected error was encountered. If this persists, our Discord community may be a good place to find some help.',
-    secondaryButtonText: 'Close',
-    onSecondaryButtonClick: () => {
-      closeErrorDialog()
+    secondaryButton: {
+      text: 'Close',
+      onClick: () => {
+        closeErrorDialog()
+      },
     },
     },
     onExitClick: () => {
     onExitClick: () => {
       closeErrorDialog()
       closeErrorDialog()
@@ -93,8 +95,10 @@ export const useTransaction = (): HandleTransactionFn => {
             variant: 'success',
             variant: 'success',
             title: successMessage.title,
             title: successMessage.title,
             description: successMessage.description,
             description: successMessage.description,
-            secondaryButtonText: 'Close',
-            onSecondaryButtonClick: handleDialogClose,
+            secondaryButton: {
+              text: 'Close',
+              onClick: handleDialogClose,
+            },
             onExitClick: handleDialogClose,
             onExitClick: handleDialogClose,
           })
           })
         })
         })

+ 2 - 2
src/shared/components/ActionBar/ActionBarTransaction.style.ts

@@ -2,7 +2,7 @@ import styled from '@emotion/styled'
 
 
 import { media, transitions, zIndex } from '@/shared/theme'
 import { media, transitions, zIndex } from '@/shared/theme'
 
 
-import { Checkout } from '../Checkout'
+import { ProgressDrawer } from '../ProgressDrawer'
 
 
 type ActionBarTransactionWrapperProps = {
 type ActionBarTransactionWrapperProps = {
   fullWidth?: boolean
   fullWidth?: boolean
@@ -23,7 +23,7 @@ export const ActionBarTransactionWrapper = styled.div<ActionBarTransactionWrappe
   }
   }
 `
 `
 
 
-export const StyledCheckout = styled(Checkout)`
+export const StyledProgressDrawer = styled(ProgressDrawer)`
   display: none;
   display: none;
   ${media.medium} {
   ${media.medium} {
     position: absolute;
     position: absolute;

+ 5 - 5
src/shared/components/ActionBar/ActionBarTransaction.tsx

@@ -1,25 +1,25 @@
 import React from 'react'
 import React from 'react'
 
 
 import { ActionBar, ActionBarProps } from './ActionBar'
 import { ActionBar, ActionBarProps } from './ActionBar'
-import { ActionBarTransactionWrapper, StyledCheckout } from './ActionBarTransaction.style'
+import { ActionBarTransactionWrapper, StyledProgressDrawer } from './ActionBarTransaction.style'
 
 
-import { Step } from '../Checkout/Checkout'
+import { Step } from '../ProgressDrawer/ProgressDrawer'
 
 
 export type ActionBarTransactionProps = {
 export type ActionBarTransactionProps = {
   fee: number
   fee: number
-  checkoutSteps?: Step[]
+  progressDrawerSteps?: Step[]
 } & Omit<ActionBarProps, 'primaryText' | 'secondaryText'>
 } & Omit<ActionBarProps, 'primaryText' | 'secondaryText'>
 
 
 export const ActionBarTransaction: React.FC<ActionBarTransactionProps> = ({
 export const ActionBarTransaction: React.FC<ActionBarTransactionProps> = ({
   fee,
   fee,
   fullWidth,
   fullWidth,
   isActive,
   isActive,
-  checkoutSteps,
+  progressDrawerSteps,
   ...actionBarArgs
   ...actionBarArgs
 }) => {
 }) => {
   return (
   return (
     <ActionBarTransactionWrapper fullWidth={fullWidth} isActive={isActive}>
     <ActionBarTransactionWrapper fullWidth={fullWidth} isActive={isActive}>
-      {checkoutSteps?.length ? <StyledCheckout steps={checkoutSteps} /> : null}
+      {progressDrawerSteps?.length ? <StyledProgressDrawer steps={progressDrawerSteps} /> : null}
       <ActionBar
       <ActionBar
         {...actionBarArgs}
         {...actionBarArgs}
         fullWidth={fullWidth}
         fullWidth={fullWidth}

+ 16 - 3
src/shared/components/Avatar/Avatar.style.tsx

@@ -5,9 +5,9 @@ import { TransitionGroup } from 'react-transition-group'
 import { SvgAvatarSilhouette } from '@/shared/illustrations'
 import { SvgAvatarSilhouette } from '@/shared/illustrations'
 import { colors, media, transitions, typography } from '@/shared/theme'
 import { colors, media, transitions, typography } from '@/shared/theme'
 
 
-import { Placeholder } from '../Placeholder'
+import { SkeletonLoader } from '../SkeletonLoader'
 
 
-export type AvatarSize = 'preview' | 'cover' | 'view' | 'default' | 'fill' | 'small'
+export type AvatarSize = 'preview' | 'cover' | 'view' | 'default' | 'fill' | 'small' | 'channel'
 
 
 type ContainerProps = {
 type ContainerProps = {
   size: AvatarSize
   size: AvatarSize
@@ -35,6 +35,17 @@ const coverAvatarCss = css`
   }
   }
 `
 `
 
 
+const channelAvatarCss = css`
+  width: 88px;
+  min-width: 88px;
+  height: 88px;
+  ${media.medium} {
+    width: 136px;
+    min-width: 136px;
+    height: 136px;
+  }
+`
+
 const viewAvatarCss = css`
 const viewAvatarCss = css`
   width: 128px;
   width: 128px;
   min-width: 128px;
   min-width: 128px;
@@ -71,6 +82,8 @@ const getAvatarSizeCss = (size: AvatarSize): SerializedStyles => {
       return coverAvatarCss
       return coverAvatarCss
     case 'view':
     case 'view':
       return viewAvatarCss
       return viewAvatarCss
+    case 'channel':
+      return channelAvatarCss
     case 'fill':
     case 'fill':
       return fillAvatarCss
       return fillAvatarCss
     case 'small':
     case 'small':
@@ -126,7 +139,7 @@ export const EditButton = styled.button<EditButtonProps>`
   }
   }
 `
 `
 
 
-export const StyledPlaceholder = styled(Placeholder)`
+export const StyledSkeletonLoader = styled(SkeletonLoader)`
   position: absolute;
   position: absolute;
   left: 0;
   left: 0;
 `
 `

+ 2 - 2
src/shared/components/Avatar/Avatar.tsx

@@ -11,7 +11,7 @@ import {
   NewChannelAvatar,
   NewChannelAvatar,
   SilhouetteAvatar,
   SilhouetteAvatar,
   StyledImage,
   StyledImage,
-  StyledPlaceholder,
+  StyledSkeletonLoader,
   StyledTransitionGroup,
   StyledTransitionGroup,
 } from './Avatar.style'
 } from './Avatar.style'
 
 
@@ -67,7 +67,7 @@ export const Avatar: React.FC<AvatarProps> = ({
             classNames={transitions.names.fade}
             classNames={transitions.names.fade}
           >
           >
             {loading ? (
             {loading ? (
-              <StyledPlaceholder rounded />
+              <StyledSkeletonLoader rounded />
             ) : assetUrl ? (
             ) : assetUrl ? (
               <StyledImage src={assetUrl} onError={onError} />
               <StyledImage src={assetUrl} onError={onError} />
             ) : hasAvatarUploadFailed ? (
             ) : hasAvatarUploadFailed ? (

+ 2 - 2
src/shared/components/CategoryPicker/CategoryPicker.style.ts

@@ -3,7 +3,7 @@ import styled from '@emotion/styled'
 
 
 import { sizes, transitions } from '@/shared/theme'
 import { sizes, transitions } from '@/shared/theme'
 
 
-import { Placeholder } from '../Placeholder'
+import { SkeletonLoader } from '../SkeletonLoader'
 import { ToggleButton } from '../ToggleButton'
 import { ToggleButton } from '../ToggleButton'
 
 
 const fadeIn = keyframes`
 const fadeIn = keyframes`
@@ -24,7 +24,7 @@ export const Container = styled.div`
   flex-wrap: wrap;
   flex-wrap: wrap;
 `
 `
 
 
-export const StyledPlaceholder = styled(Placeholder)`
+export const StyledSkeletonLoader = styled(SkeletonLoader)`
   margin: 0 ${sizes(3)} ${sizes(3)} 0;
   margin: 0 ${sizes(3)} ${sizes(3)} 0;
 `
 `
 
 

+ 2 - 2
src/shared/components/CategoryPicker/CategoryPicker.tsx

@@ -2,7 +2,7 @@ import React from 'react'
 
 
 import { VideoCategoryFieldsFragment } from '@/api/queries'
 import { VideoCategoryFieldsFragment } from '@/api/queries'
 
 
-import { Container, StyledPlaceholder, StyledToggleButton } from './CategoryPicker.style'
+import { Container, StyledSkeletonLoader, StyledToggleButton } from './CategoryPicker.style'
 
 
 type CategoryPickerProps = {
 type CategoryPickerProps = {
   categories?: VideoCategoryFieldsFragment[]
   categories?: VideoCategoryFieldsFragment[]
@@ -37,7 +37,7 @@ export const CategoryPicker: React.FC<CategoryPickerProps> = ({
     <Container className={className}>
     <Container className={className}>
       {isLoading
       {isLoading
         ? CATEGORY_PLACEHOLDER_WIDTHS.map((width, idx) => (
         ? CATEGORY_PLACEHOLDER_WIDTHS.map((width, idx) => (
-            <StyledPlaceholder key={`placeholder-${idx}`} width={width} height="48px" />
+            <StyledSkeletonLoader key={`placeholder-${idx}`} width={width} height="48px" />
           ))
           ))
         : displayedCategories.map((category) => (
         : displayedCategories.map((category) => (
             <StyledToggleButton
             <StyledToggleButton

+ 6 - 6
src/shared/components/ChannelPreviewBase/ChannelPreview.stories.tsx → src/shared/components/ChannelCardBase/ChannelCard.stories.tsx

@@ -2,11 +2,11 @@ import { Meta, Story } from '@storybook/react'
 import React from 'react'
 import React from 'react'
 import { BrowserRouter } from 'react-router-dom'
 import { BrowserRouter } from 'react-router-dom'
 
 
-import { ChannelPreviewBase, ChannelPreviewBaseProps } from './ChannelPreviewBase'
+import { ChannelCardBase, ChannelCardBaseProps } from './ChannelCardBase'
 
 
 export default {
 export default {
-  title: 'Shared/C/ChannelPreview',
-  component: ChannelPreviewBase,
+  title: 'Shared/C/ChannelCard',
+  component: ChannelCardBase,
   argTypes: {
   argTypes: {
     className: { table: { disable: true } },
     className: { table: { disable: true } },
     onClick: { table: { disable: true } },
     onClick: { table: { disable: true } },
@@ -14,8 +14,8 @@ export default {
   decorators: [(story) => <BrowserRouter>{story()}</BrowserRouter>],
   decorators: [(story) => <BrowserRouter>{story()}</BrowserRouter>],
 } as Meta
 } as Meta
 
 
-const Template: Story<ChannelPreviewBaseProps> = (args) => <ChannelPreviewBase {...args} />
-const PlaceholderTemplate: Story<ChannelPreviewBaseProps> = (args) => <ChannelPreviewBase {...args} />
+const Template: Story<ChannelCardBaseProps> = (args) => <ChannelCardBase {...args} />
+const SkeletonLoaderTemplate: Story<ChannelCardBaseProps> = (args) => <ChannelCardBase {...args} />
 
 
 export const Regular = Template.bind({})
 export const Regular = Template.bind({})
 Regular.args = {
 Regular.args = {
@@ -24,4 +24,4 @@ Regular.args = {
   videoCount: 0,
   videoCount: 0,
   loading: false,
   loading: false,
 }
 }
-export const Placeholder = PlaceholderTemplate.bind({})
+export const SkeletonLoader = SkeletonLoaderTemplate.bind({})

+ 0 - 0
src/shared/components/ChannelPreviewBase/ChannelPreviewBase.style.tsx → src/shared/components/ChannelCardBase/ChannelCardBase.style.tsx


+ 8 - 8
src/shared/components/ChannelPreviewBase/ChannelPreviewBase.tsx → src/shared/components/ChannelCardBase/ChannelCardBase.tsx

@@ -13,11 +13,11 @@ import {
   TextBase,
   TextBase,
   VideoCount,
   VideoCount,
   VideoCountContainer,
   VideoCountContainer,
-} from './ChannelPreviewBase.style'
+} from './ChannelCardBase.style'
 
 
-import { Placeholder } from '../Placeholder'
+import { SkeletonLoader } from '../SkeletonLoader'
 
 
-export type ChannelPreviewBaseProps = {
+export type ChannelCardBaseProps = {
   assetUrl?: string | null
   assetUrl?: string | null
   title?: string | null
   title?: string | null
   videoCount?: number
   videoCount?: number
@@ -27,7 +27,7 @@ export type ChannelPreviewBaseProps = {
   onClick?: (e: React.MouseEvent<HTMLElement>) => void
   onClick?: (e: React.MouseEvent<HTMLElement>) => void
 }
 }
 
 
-export const ChannelPreviewBase: React.FC<ChannelPreviewBaseProps> = ({
+export const ChannelCardBase: React.FC<ChannelCardBaseProps> = ({
   assetUrl,
   assetUrl,
   title,
   title,
   videoCount,
   videoCount,
@@ -57,24 +57,24 @@ export const ChannelPreviewBase: React.FC<ChannelPreviewBaseProps> = ({
           >
           >
             <InnerContainer animated={isAnimated}>
             <InnerContainer animated={isAnimated}>
               <AvatarContainer>
               <AvatarContainer>
-                {loading ? <Placeholder rounded /> : <StyledAvatar assetUrl={assetUrl} />}
+                {loading ? <SkeletonLoader rounded /> : <StyledAvatar assetUrl={assetUrl} />}
               </AvatarContainer>
               </AvatarContainer>
               <Info>
               <Info>
                 {loading ? (
                 {loading ? (
-                  <Placeholder width="140px" height="16px" />
+                  <SkeletonLoader width="140px" height="16px" />
                 ) : (
                 ) : (
                   <TextBase variant="h6">{title || '\u00A0'}</TextBase>
                   <TextBase variant="h6">{title || '\u00A0'}</TextBase>
                 )}
                 )}
                 <VideoCountContainer>
                 <VideoCountContainer>
                   {loading ? (
                   {loading ? (
-                    <Placeholder width="140px" height="16px" />
+                    <SkeletonLoader width="140px" height="16px" />
                   ) : (
                   ) : (
                     <CSSTransition
                     <CSSTransition
                       in={!!videoCount}
                       in={!!videoCount}
                       timeout={parseInt(transitions.timings.loading) * 0.5}
                       timeout={parseInt(transitions.timings.loading) * 0.5}
                       classNames={transitions.names.fade}
                       classNames={transitions.names.fade}
                     >
                     >
-                      <VideoCount variant="subtitle2">{videoCount ? `${videoCount} Uploads` : '⠀'}</VideoCount>
+                      <VideoCount variant="subtitle2">{`${videoCount ?? ''} Uploads`}</VideoCount>
                     </CSSTransition>
                     </CSSTransition>
                   )}
                   )}
                 </VideoCountContainer>
                 </VideoCountContainer>

+ 1 - 0
src/shared/components/ChannelCardBase/index.ts

@@ -0,0 +1 @@
+export * from './ChannelCardBase'

+ 3 - 93
src/shared/components/ChannelCover/ChannelCover.style.ts

@@ -13,12 +13,6 @@ export const CONTENT_OVERLAP_MAP = {
   XLARGE: 200,
   XLARGE: 200,
   XXLARGE: 300,
   XXLARGE: 300,
 }
 }
-const GRADIENT_OVERLAP = 50
-const GRADIENT_HEIGHT = 100
-
-type CoverImageProps = {
-  $src: string
-}
 
 
 export const MediaWrapper = styled.div`
 export const MediaWrapper = styled.div`
   margin: 0 calc(-1 * var(--global-horizontal-padding));
   margin: 0 calc(-1 * var(--global-horizontal-padding));
@@ -32,100 +26,20 @@ export const Media = styled.div`
   padding-top: 25%;
   padding-top: 25%;
   position: relative;
   position: relative;
   z-index: ${zIndex.background};
   z-index: ${zIndex.background};
+  overflow: hidden;
 `
 `
 
 
-export const CoverImage = styled.div<CoverImageProps>`
+export const CoverImage = styled.img`
+  width: 100%;
   position: absolute;
   position: absolute;
   top: 0;
   top: 0;
   right: 0;
   right: 0;
   bottom: 0;
   bottom: 0;
   left: 0;
   left: 0;
-  background-repeat: no-repeat;
-  background-position: center;
-  background-attachment: local;
-  background-size: cover;
-
-  /* as the content overlaps the media more and more as the viewport width grows, we need to hide some part of the media with a gradient
-  this helps with keeping a consistent background behind a page content - we don\'t want the media to peek out in the content spacing */
-  background-image: linear-gradient(0deg, black 0%, rgba(0, 0, 0, 0) ${GRADIENT_HEIGHT / 4}px),
-    url(${({ $src }) => $src});
-
-  ${media.small} {
-    background-image: linear-gradient(
-        0deg,
-        black 0%,
-        black ${Math.min(CONTENT_OVERLAP_MAP.SMALL - GRADIENT_OVERLAP, 0)}px,
-        rgba(0, 0, 0, 0) ${CONTENT_OVERLAP_MAP.SMALL - GRADIENT_OVERLAP + GRADIENT_HEIGHT}px
-      ),
-      url(${({ $src }) => $src});
-  }
-
-  ${media.medium} {
-    background-image: linear-gradient(
-        0deg,
-        black 0%,
-        black ${Math.min(CONTENT_OVERLAP_MAP.MEDIUM - GRADIENT_OVERLAP, 0)}px,
-        rgba(0, 0, 0, 0) ${CONTENT_OVERLAP_MAP.MEDIUM - GRADIENT_OVERLAP + GRADIENT_HEIGHT}px
-      ),
-      url(${({ $src }) => $src});
-  }
-
-  ${media.large} {
-    background-image: linear-gradient(
-        0deg,
-        black 0%,
-        black ${CONTENT_OVERLAP_MAP.LARGE - GRADIENT_OVERLAP}px,
-        rgba(0, 0, 0, 0) ${CONTENT_OVERLAP_MAP.LARGE - GRADIENT_OVERLAP + GRADIENT_HEIGHT}px
-      ),
-      url(${({ $src }) => $src});
-  }
-
-  ${media.xlarge} {
-    background-image: linear-gradient(
-        0deg,
-        black 0%,
-        black ${CONTENT_OVERLAP_MAP.XLARGE - GRADIENT_OVERLAP}px,
-        rgba(0, 0, 0, 0) ${CONTENT_OVERLAP_MAP.XLARGE - GRADIENT_OVERLAP + GRADIENT_HEIGHT}px
-      ),
-      url(${({ $src }) => $src});
-  }
-
-  ${media.xxlarge} {
-    background-image: linear-gradient(
-        0deg,
-        black 0%,
-        black ${CONTENT_OVERLAP_MAP.XXLARGE - GRADIENT_OVERLAP}px,
-        rgba(0, 0, 0, 0) ${CONTENT_OVERLAP_MAP.XXLARGE - GRADIENT_OVERLAP + GRADIENT_HEIGHT}px
-      ),
-      url(${({ $src }) => $src});
-  }
 `
 `
 
 
 export const CoverWrapper = styled.div`
 export const CoverWrapper = styled.div`
   position: relative;
   position: relative;
-
-  /* because of the fixed aspect ratio, as the viewport width grows, the media will occupy more height as well
-   so that the media doesn't take too big of a portion of the space, we let the content overlap the media via a negative margin */
-  margin-bottom: -${CONTENT_OVERLAP_MAP.BASE}px;
-  ${media.small} {
-    margin-bottom: -${CONTENT_OVERLAP_MAP.SMALL}px;
-  }
-
-  ${media.medium} {
-    margin-bottom: -${CONTENT_OVERLAP_MAP.MEDIUM}px;
-  }
-
-  ${media.large} {
-    margin-bottom: -${CONTENT_OVERLAP_MAP.LARGE}px;
-  }
-
-  ${media.xlarge} {
-    margin-bottom: -${CONTENT_OVERLAP_MAP.XLARGE}px;
-  }
-
-  ${media.xxlarge} {
-    margin-bottom: -${CONTENT_OVERLAP_MAP.XXLARGE}px;
-  }
 `
 `
 
 
 export const EditableControls = styled.div`
 export const EditableControls = styled.div`
@@ -144,10 +58,6 @@ export const EditableControls = styled.div`
       opacity: 1;
       opacity: 1;
     }
     }
   }
   }
-
-  ${media.xlarge} {
-    height: 80%;
-  }
 `
 `
 
 
 export const EditCoverDesktopOverlay = styled.div`
 export const EditCoverDesktopOverlay = styled.div`

+ 1 - 1
src/shared/components/ChannelCover/ChannelCover.tsx

@@ -56,7 +56,7 @@ export const ChannelCover: React.FC<ChannelCoverProps> = ({
               classNames={transitions.names.fade}
               classNames={transitions.names.fade}
             >
             >
               {assetUrl ? (
               {assetUrl ? (
-                <CoverImage $src={assetUrl} />
+                <CoverImage src={assetUrl} />
               ) : hasCoverUploadFailed ? (
               ) : hasCoverUploadFailed ? (
                 <FailedUploadContainer>
                 <FailedUploadContainer>
                   <SvgLargeUploadFailed />
                   <SvgLargeUploadFailed />

+ 0 - 1
src/shared/components/ChannelPreviewBase/index.ts

@@ -1 +0,0 @@
-export * from './ChannelPreviewBase'

+ 0 - 1
src/shared/components/Checkout/index.tsx

@@ -1 +0,0 @@
-export * from './Checkout'

+ 7 - 7
src/shared/components/CircularProgressbar/CircularProgressbar.stories.tsx → src/shared/components/CircularProgress/CircularProgress.stories.tsx

@@ -1,24 +1,24 @@
 import { Meta, Story } from '@storybook/react'
 import { Meta, Story } from '@storybook/react'
 import React from 'react'
 import React from 'react'
 
 
-import { CircularProgressbar, CircularProgressbarProps } from './CircularProgressbar'
+import { CircularProgress, CircularProgressProps } from './CircularProgress'
 
 
 export default {
 export default {
-  title: 'Shared/C/CircularProgressbar',
-  component: CircularProgressbar,
+  title: 'Shared/C/CircularProgress',
+  component: CircularProgress,
   argTypes: {},
   argTypes: {},
 } as Meta
 } as Meta
 
 
-const SingleTemplate: Story<CircularProgressbarProps> = (args) => (
+const SingleTemplate: Story<CircularProgressProps> = (args) => (
   <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
   <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
     <div style={{ width: '24px', height: '24px' }}>
     <div style={{ width: '24px', height: '24px' }}>
-      <CircularProgressbar {...args}></CircularProgressbar>
+      <CircularProgress {...args}></CircularProgress>
     </div>
     </div>
     <div style={{ width: '48px', height: '48px' }}>
     <div style={{ width: '48px', height: '48px' }}>
-      <CircularProgressbar {...args}></CircularProgressbar>
+      <CircularProgress {...args}></CircularProgress>
     </div>
     </div>
     <div style={{ width: '96px', height: '96px' }}>
     <div style={{ width: '96px', height: '96px' }}>
-      <CircularProgressbar {...args}></CircularProgressbar>
+      <CircularProgress {...args}></CircularProgress>
     </div>
     </div>
   </div>
   </div>
 )
 )

+ 0 - 0
src/shared/components/CircularProgressbar/CircularProgressbar.style.tsx → src/shared/components/CircularProgress/CircularProgress.style.tsx


+ 4 - 4
src/shared/components/CircularProgressbar/CircularProgressbar.tsx → src/shared/components/CircularProgress/CircularProgress.tsx

@@ -1,6 +1,6 @@
 import * as React from 'react'
 import * as React from 'react'
 
 
-import { Background, SVG, StyledPath, Trail, TrailVariant } from './CircularProgressbar.style'
+import { Background, SVG, StyledPath, Trail, TrailVariant } from './CircularProgress.style'
 
 
 export const VIEWBOX_WIDTH = 100
 export const VIEWBOX_WIDTH = 100
 export const VIEWBOX_HEIGHT = 100
 export const VIEWBOX_HEIGHT = 100
@@ -8,7 +8,7 @@ export const VIEWBOX_HEIGHT_HALF = VIEWBOX_HEIGHT / 2
 export const VIEWBOX_CENTER_X = VIEWBOX_WIDTH / 2
 export const VIEWBOX_CENTER_X = VIEWBOX_WIDTH / 2
 export const VIEWBOX_CENTER_Y = VIEWBOX_HEIGHT / 2
 export const VIEWBOX_CENTER_Y = VIEWBOX_HEIGHT / 2
 
 
-export type CircularProgressbarProps = {
+export type CircularProgressProps = {
   value: number
   value: number
   circleRatio?: number
   circleRatio?: number
   counterClockwise?: boolean
   counterClockwise?: boolean
@@ -22,7 +22,7 @@ export type CircularProgressbarProps = {
   noTrail?: boolean
   noTrail?: boolean
 }
 }
 
 
-export const CircularProgressbar: React.FC<CircularProgressbarProps> = ({
+export const CircularProgress: React.FC<CircularProgressProps> = ({
   value,
   value,
   background = false,
   background = false,
   backgroundPadding = 0,
   backgroundPadding = 0,
@@ -50,7 +50,7 @@ export const CircularProgressbar: React.FC<CircularProgressbarProps> = ({
     <>
     <>
       <SVG viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`} className={className}>
       <SVG viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`} className={className}>
         {background ? <Background cx={VIEWBOX_CENTER_X} cy={VIEWBOX_CENTER_Y} r={VIEWBOX_HEIGHT_HALF} /> : null}
         {background ? <Background cx={VIEWBOX_CENTER_X} cy={VIEWBOX_CENTER_Y} r={VIEWBOX_HEIGHT_HALF} /> : null}
-        {noTrail && (
+        {!noTrail && (
           <Trail
           <Trail
             counterClockwise={counterClockwise}
             counterClockwise={counterClockwise}
             dashRatio={circleRatio}
             dashRatio={circleRatio}

+ 1 - 1
src/shared/components/CircularProgressbar/Path.tsx → src/shared/components/CircularProgress/Path.tsx

@@ -1,6 +1,6 @@
 import React from 'react'
 import React from 'react'
 
 
-import { VIEWBOX_CENTER_X, VIEWBOX_CENTER_Y } from './CircularProgressbar'
+import { VIEWBOX_CENTER_X, VIEWBOX_CENTER_Y } from './CircularProgress'
 
 
 type PathProps = {
 type PathProps = {
   className?: string
   className?: string

+ 1 - 0
src/shared/components/CircularProgress/index.ts

@@ -0,0 +1 @@
+export * from './CircularProgress'

+ 0 - 1
src/shared/components/CircularProgressbar/index.ts

@@ -1 +0,0 @@
-export * from './CircularProgressbar'

+ 5 - 5
src/shared/components/DismissibleMessage/DismissibleMessage.stories.tsx → src/shared/components/DismissibleBanner/DismissibleBanner.stories.tsx

@@ -3,13 +3,13 @@ import React from 'react'
 
 
 import { usePersonalDataStore } from '@/providers'
 import { usePersonalDataStore } from '@/providers'
 
 
-import { DismissibleMessage, DismissibleMessageProps } from './DismissibleMessage'
+import { DismissibleBanner, DismissibleBannerProps } from './DismissibleBanner'
 
 
 import { Button } from '../Button'
 import { Button } from '../Button'
 
 
 export default {
 export default {
-  title: 'Shared/D/DismissibleMessage',
-  component: DismissibleMessage,
+  title: 'Shared/D/DismissibleBanner',
+  component: DismissibleBanner,
   argTypes: {
   argTypes: {
     id: {
     id: {
       defaultValue: 'video-drafts',
       defaultValue: 'video-drafts',
@@ -34,11 +34,11 @@ export default {
   decorators: [(Story) => <Story />],
   decorators: [(Story) => <Story />],
 } as Meta
 } as Meta
 
 
-const Template: Story<DismissibleMessageProps> = (args) => {
+const Template: Story<DismissibleBannerProps> = (args) => {
   const updateDismissedMessages = usePersonalDataStore((state) => state.actions.updateDismissedMessages)
   const updateDismissedMessages = usePersonalDataStore((state) => state.actions.updateDismissedMessages)
   return (
   return (
     <>
     <>
-      <DismissibleMessage {...args} />
+      <DismissibleBanner {...args} />
       <Button
       <Button
         onClick={() => {
         onClick={() => {
           updateDismissedMessages(args.id, false)
           updateDismissedMessages(args.id, false)

+ 2 - 2
src/shared/components/DismissibleMessage/DismissibleMessage.tsx → src/shared/components/DismissibleBanner/DismissibleBanner.tsx

@@ -4,11 +4,11 @@ import { usePersonalDataStore } from '@/providers'
 
 
 import { Banner, BannerProps } from '../Banner'
 import { Banner, BannerProps } from '../Banner'
 
 
-export type DismissibleMessageProps = {
+export type DismissibleBannerProps = {
   id: string
   id: string
 } & Omit<BannerProps, 'onExitClick'>
 } & Omit<BannerProps, 'onExitClick'>
 
 
-export const DismissibleMessage: React.FC<DismissibleMessageProps> = ({ id, ...dismissedMessageProps }) => {
+export const DismissibleBanner: React.FC<DismissibleBannerProps> = ({ id, ...dismissedMessageProps }) => {
   const isDismissedMessage = usePersonalDataStore((state) =>
   const isDismissedMessage = usePersonalDataStore((state) =>
     state.dismissedMessages.some((message) => message.id === id)
     state.dismissedMessages.some((message) => message.id === id)
   )
   )

+ 1 - 0
src/shared/components/DismissibleBanner/index.ts

@@ -0,0 +1 @@
+export * from './DismissibleBanner'

+ 0 - 1
src/shared/components/DismissibleMessage/index.ts

@@ -1 +0,0 @@
-export * from './DismissibleMessage'

+ 38 - 0
src/shared/components/EmptyFallback/EmptyFallback.stories.tsx

@@ -0,0 +1,38 @@
+import { Story } from '@storybook/react'
+import React from 'react'
+
+import { Button } from '@/shared/components'
+import { SvgGlyphUpload } from '@/shared/icons'
+
+import { EmptyFallback, EmptyFallbackProps } from './EmptyFallback'
+
+export default {
+  title: 'Shared/E/EmptyFallback',
+  argTypes: {
+    title: {
+      control: { type: 'text' },
+      defaultValue: 'No draft here yet',
+    },
+    subtitle: {
+      control: { type: 'text' },
+      defaultValue: 'Each unfinished project will be saved here as a draft. Start publishing to see something here.',
+    },
+    variant: {
+      control: { type: 'select', options: ['small', 'large'] },
+      defaultValue: 'large',
+    },
+  },
+}
+
+const Template: Story<EmptyFallbackProps> = (args) => (
+  <EmptyFallback
+    {...args}
+    button={
+      <Button icon={<SvgGlyphUpload />} variant="secondary" size="large">
+        Upload video
+      </Button>
+    }
+  />
+)
+
+export const Default = Template.bind({})

+ 40 - 0
src/shared/components/EmptyFallback/EmptyFallback.styles.ts

@@ -0,0 +1,40 @@
+import styled from '@emotion/styled'
+
+import { media, sizes } from '@/shared/theme'
+
+import { EmptyFallbackSizes } from './EmptyFallback'
+
+import { Text } from '../Text'
+
+export const Container = styled.div<{ variant?: EmptyFallbackSizes }>`
+  margin: ${sizes(10)} auto;
+  display: grid;
+  place-items: center;
+
+  ${media.compact} {
+    width: ${({ variant }) => (variant === 'large' ? sizes(90) : 'auto')};
+  }
+
+  ${({ variant }) => `
+    ${Title} {
+      margin-top: ${sizes(variant === 'large' ? 10 : 6)};
+  `}
+`
+
+export const Message = styled.div`
+  display: flex;
+  flex-direction: column;
+  text-align: center;
+`
+
+export const Title = styled(Text)`
+  line-height: 1.25;
+`
+export const Subtitle = styled(Text)`
+  margin-top: ${sizes(2)};
+  line-height: ${sizes(5)};
+`
+
+export const ButtonWrapper = styled.div`
+  margin-top: ${sizes(6)};
+`

+ 40 - 0
src/shared/components/EmptyFallback/EmptyFallback.tsx

@@ -0,0 +1,40 @@
+import React, { FC, ReactNode } from 'react'
+
+import { SvgEmptyStateIllustration } from '@/shared/illustrations'
+
+import { ButtonWrapper, Container, Message, Subtitle, Title } from './EmptyFallback.styles'
+
+export type EmptyFallbackSizes = 'small' | 'large'
+
+export type EmptyFallbackProps = {
+  title: string
+  subtitle?: string | null
+  variant?: EmptyFallbackSizes
+  button?: ReactNode
+}
+
+const ILLUSTRATION_SIZES = {
+  small: {
+    width: 180,
+    height: 114,
+  },
+  large: {
+    width: 240,
+    height: 152,
+  },
+}
+
+export const EmptyFallback: FC<EmptyFallbackProps> = ({ title, subtitle, variant = 'large', button }) => (
+  <Container variant={variant}>
+    <SvgEmptyStateIllustration width={ILLUSTRATION_SIZES[variant].width} height={ILLUSTRATION_SIZES[variant].height} />
+    <Message>
+      {title && <Title variant={variant === 'large' ? 'h4' : 'body1'}>{title}</Title>}
+      {subtitle && (
+        <Subtitle variant="body2" secondary>
+          {subtitle}
+        </Subtitle>
+      )}
+    </Message>
+    <ButtonWrapper>{button}</ButtonWrapper>
+  </Container>
+)

+ 1 - 0
src/shared/components/EmptyFallback/index.ts

@@ -0,0 +1 @@
+export * from './EmptyFallback'

+ 2 - 2
src/shared/components/FileStep/FileStep.style.ts

@@ -2,7 +2,7 @@ import styled from '@emotion/styled'
 
 
 import { colors, sizes, transitions, typography } from '@/shared/theme'
 import { colors, sizes, transitions, typography } from '@/shared/theme'
 
 
-import { CircularProgressbar } from '../CircularProgressbar'
+import { CircularProgress } from '../CircularProgress'
 import { Text } from '../Text'
 import { Text } from '../Text'
 
 
 type StepProps = {
 type StepProps = {
@@ -77,7 +77,7 @@ export const FileName = styled(Text)`
   text-overflow: ellipsis;
   text-overflow: ellipsis;
 `
 `
 
 
-export const StyledProgress = styled(CircularProgressbar)`
+export const StyledProgress = styled(CircularProgress)`
   width: ${sizes(7)};
   width: ${sizes(7)};
   height: ${sizes(7)};
   height: ${sizes(7)};
 `
 `

+ 2 - 2
src/shared/components/Grid/Grid.tsx

@@ -4,7 +4,7 @@ import React, { useRef } from 'react'
 import useResizeObserver from 'use-resize-observer'
 import useResizeObserver from 'use-resize-observer'
 
 
 import { media, sizes } from '../../theme'
 import { media, sizes } from '../../theme'
-import { MIN_VIDEO_PREVIEW_WIDTH } from '../VideoPreviewBase'
+import { MIN_VIDEO_TILE_WIDTH } from '../VideoTileBase'
 
 
 const toPx = (n: number | string) => (typeof n === 'number' ? `${n}px` : n)
 const toPx = (n: number | string) => (typeof n === 'number' ? `${n}px` : n)
 
 
@@ -23,7 +23,7 @@ export const Grid: React.FC<GridProps> = ({
   onResize,
   onResize,
   repeat = 'fill',
   repeat = 'fill',
   maxColumns = 6,
   maxColumns = 6,
-  minWidth = MIN_VIDEO_PREVIEW_WIDTH,
+  minWidth = MIN_VIDEO_TILE_WIDTH,
   ...props
   ...props
 }) => {
 }) => {
   const gridRef = useRef<HTMLImageElement>(null)
   const gridRef = useRef<HTMLImageElement>(null)

+ 0 - 1
src/shared/components/HeaderTextField/index.ts

@@ -1 +0,0 @@
-export * from './HeaderTextField'

+ 0 - 32
src/shared/components/Placeholder/Placeholder.tsx

@@ -1,32 +0,0 @@
-import { keyframes } from '@emotion/react'
-import styled from '@emotion/styled'
-import { darken } from 'polished'
-
-import { colors } from '@/shared/theme'
-
-type PlaceholderProps = {
-  width?: string | number
-  height?: string | number
-  bottomSpace?: string | number
-  rounded?: boolean
-}
-
-const getPropValue = (v: string | number) => (typeof v === 'string' ? v : `${v}px`)
-
-const pulse = keyframes`
-  0, 100% { 
-    background-color: ${colors.gray[400]}
-  }
-  50% {
-    background-color: ${darken(0.15, colors.gray[400])}
-  }
-`
-
-export const Placeholder = styled.div<PlaceholderProps>`
-  width: ${({ width = '100%' }) => getPropValue(width)};
-  height: ${({ height = '100%' }) => getPropValue(height)};
-  margin-bottom: ${({ bottomSpace = 0 }) => getPropValue(bottomSpace)};
-  border-radius: ${({ rounded = false }) => (rounded ? '100%' : '0')};
-  background-color: ${colors.gray['400']};
-  animation: ${pulse} 0.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
-`

+ 0 - 1
src/shared/components/Placeholder/index.ts

@@ -1 +0,0 @@
-export * from './Placeholder'

+ 5 - 5
src/shared/components/Checkout/Checkout.stories.tsx → src/shared/components/ProgressDrawer/ProgressDrawer.stories.tsx

@@ -1,15 +1,15 @@
 import { Meta, Story } from '@storybook/react'
 import { Meta, Story } from '@storybook/react'
 import React, { useState } from 'react'
 import React, { useState } from 'react'
 
 
-import { Checkout, CheckoutProps } from './Checkout'
+import { ProgressDrawer, ProgressDrawerProps } from './ProgressDrawer'
 
 
 export default {
 export default {
-  title: 'Shared/C/Checkout',
-  component: Checkout,
+  title: 'Shared/P/ProgressDrawer',
+  component: ProgressDrawer,
   argTypes: {},
   argTypes: {},
 } as Meta
 } as Meta
 
 
-const SingleTemplate: Story<CheckoutProps> = (args) => {
+const SingleTemplate: Story<ProgressDrawerProps> = (args) => {
   const [addChannel, setaddChannel] = useState(true)
   const [addChannel, setaddChannel] = useState(true)
   const [addDescription, setaddDescription] = useState(false)
   const [addDescription, setaddDescription] = useState(false)
   const [addAvatar, setaddAvatar] = useState(false)
   const [addAvatar, setaddAvatar] = useState(false)
@@ -21,7 +21,7 @@ const SingleTemplate: Story<CheckoutProps> = (args) => {
     { title: 'Add Avatar', onClick: () => setaddAvatar((value) => !value), completed: addAvatar },
     { title: 'Add Avatar', onClick: () => setaddAvatar((value) => !value), completed: addAvatar },
     { title: 'Add Cover Image', onClick: () => setaddCoverImage((value) => !value), completed: addCoverImage },
     { title: 'Add Cover Image', onClick: () => setaddCoverImage((value) => !value), completed: addCoverImage },
   ]
   ]
-  return <Checkout {...args} steps={steps}></Checkout>
+  return <ProgressDrawer {...args} steps={steps}></ProgressDrawer>
 }
 }
 
 
 export const Single = SingleTemplate.bind({})
 export const Single = SingleTemplate.bind({})

+ 2 - 2
src/shared/components/Checkout/Checkout.styles.tsx → src/shared/components/ProgressDrawer/ProgressDrawer.styles.tsx

@@ -4,7 +4,7 @@ import styled from '@emotion/styled'
 import { SvgGlyphChevronDown } from '@/shared/icons'
 import { SvgGlyphChevronDown } from '@/shared/icons'
 import { colors, sizes, transitions } from '@/shared/theme'
 import { colors, sizes, transitions } from '@/shared/theme'
 
 
-import { CircularProgressbar } from '../CircularProgressbar'
+import { CircularProgress } from '../CircularProgress'
 import { Text } from '../Text'
 import { Text } from '../Text'
 
 
 export const Container = styled.div`
 export const Container = styled.div`
@@ -16,7 +16,7 @@ export const CircularProgresaBarContainer = styled.div`
   align-items: center;
   align-items: center;
 `
 `
 
 
-export const StyledCircularProgressbar = styled(CircularProgressbar)`
+export const StyledCircularProgress = styled(CircularProgress)`
   margin-left: ${sizes(2)};
   margin-left: ${sizes(2)};
   margin-right: ${sizes(2)};
   margin-right: ${sizes(2)};
   width: ${sizes(6)};
   width: ${sizes(6)};

+ 5 - 5
src/shared/components/Checkout/Checkout.tsx → src/shared/components/ProgressDrawer/ProgressDrawer.tsx

@@ -14,19 +14,19 @@ import {
   StepsCompletedText,
   StepsCompletedText,
   StepsContainer,
   StepsContainer,
   StepsProgressContainer,
   StepsProgressContainer,
-  StyledCircularProgressbar,
+  StyledCircularProgress,
   StyledSvgGlyphChevronDown,
   StyledSvgGlyphChevronDown,
-} from './Checkout.styles'
+} from './ProgressDrawer.styles'
 
 
 import { IconButton } from '../IconButton'
 import { IconButton } from '../IconButton'
 import { Text } from '../Text'
 import { Text } from '../Text'
 
 
 export type Step = { title: string; onClick: () => void; completed: boolean }
 export type Step = { title: string; onClick: () => void; completed: boolean }
-export type CheckoutProps = {
+export type ProgressDrawerProps = {
   steps: Step[]
   steps: Step[]
   className?: string
   className?: string
 }
 }
-export const Checkout: React.FC<CheckoutProps> = ({ steps, className }) => {
+export const ProgressDrawer: React.FC<ProgressDrawerProps> = ({ steps, className }) => {
   const stepsCompletedNumber = steps.filter(({ completed }) => completed).length
   const stepsCompletedNumber = steps.filter(({ completed }) => completed).length
   const [isHidden, setIsHidden] = useState(false)
   const [isHidden, setIsHidden] = useState(false)
   return (
   return (
@@ -51,7 +51,7 @@ export const Checkout: React.FC<CheckoutProps> = ({ steps, className }) => {
       <BottomRowContainer>
       <BottomRowContainer>
         <StepsProgressContainer>
         <StepsProgressContainer>
           <CircularProgresaBarContainer>
           <CircularProgresaBarContainer>
-            <StyledCircularProgressbar value={stepsCompletedNumber} maxValue={steps.length} />
+            <StyledCircularProgress value={stepsCompletedNumber} maxValue={steps.length} />
             <StepsCompletedText variant="body2">
             <StepsCompletedText variant="body2">
               {stepsCompletedNumber}/{steps.length}
               {stepsCompletedNumber}/{steps.length}
             </StepsCompletedText>
             </StepsCompletedText>

+ 1 - 0
src/shared/components/ProgressDrawer/index.tsx

@@ -0,0 +1 @@
+export * from './ProgressDrawer'

+ 1 - 1
src/shared/components/Select/Select.style.ts

@@ -44,7 +44,7 @@ export const SelectMenu = styled.ul<SelectMenuProps>`
   width: 100%;
   width: 100%;
   max-height: 300px;
   max-height: 300px;
   position: absolute;
   position: absolute;
-  overflow-y: scroll;
+  overflow-y: auto;
   z-index: 1;
   z-index: 1;
   padding: 0;
   padding: 0;
   margin-top: ${({ isOpen }) => (isOpen ? sizes(1) : 0)};
   margin-top: ${({ isOpen }) => (isOpen ? sizes(1) : 0)};

+ 31 - 0
src/shared/components/SkeletonLoader/SkeletonLoader.stories.tsx

@@ -0,0 +1,31 @@
+import { Meta, Story } from '@storybook/react'
+import React from 'react'
+
+import { SkeletonLoader, SkeletonLoaderProps } from './SkeletonLoader'
+
+export default {
+  title: 'Shared/S/SkeletonLoader',
+  component: SkeletonLoader,
+  argTypes: {
+    width: {
+      defaultValue: 500,
+      control: {
+        type: 'range',
+        min: 200,
+        max: 500,
+      },
+    },
+    height: {
+      defaultValue: 200,
+      control: {
+        type: 'range',
+        min: 200,
+        max: 500,
+      },
+    },
+  },
+} as Meta
+
+const Template: Story<SkeletonLoaderProps> = (args) => <SkeletonLoader {...args} />
+
+export const Default = Template.bind({})

+ 62 - 0
src/shared/components/SkeletonLoader/SkeletonLoader.tsx

@@ -0,0 +1,62 @@
+import { keyframes } from '@emotion/react'
+import styled from '@emotion/styled'
+import React from 'react'
+
+import { colors } from '@/shared/theme'
+
+export type SkeletonLoaderProps = {
+  width?: string | number
+  height?: string | number
+  bottomSpace?: string | number
+  rounded?: boolean
+} & React.HTMLAttributes<HTMLDivElement>
+
+const getPropValue = (v: string | number) => (typeof v === 'string' ? v : `${v}px`)
+
+const pulse = keyframes`
+  0% {
+    transform: translateX(0%);
+  }
+  49.999% {
+    transform: translateX(100%);
+  }
+  50% {
+    transform: translateX(-100%);
+  }
+`
+
+export const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({ className, ...props }) => (
+  <SkeletonLoaderContainer {...props} className={className}>
+    <SkeletonLoaderAnimated {...props} />
+  </SkeletonLoaderContainer>
+)
+
+const SkeletonLoaderContainer = styled.div<SkeletonLoaderProps>`
+  width: ${({ width = '100%' }) => getPropValue(width)};
+  height: ${({ height = '100%' }) => getPropValue(height)};
+  margin-bottom: ${({ bottomSpace = 0 }) => getPropValue(bottomSpace)};
+  border-radius: ${({ rounded = false }) => (rounded ? '100%' : '0')};
+  background-color: ${colors.gray['800']};
+  overflow: hidden;
+
+  /* Safari fix
+     https://stackoverflow.com/questions/49066011/overflow-hidden-with-border-radius-not-working-on-safari */
+  transform: translateZ(0);
+`
+
+const SkeletonLoaderAnimated = styled.div<SkeletonLoaderProps>`
+  height: ${({ height = '100%' }) => getPropValue(height)};
+  transform: translateX(0%);
+  background: linear-gradient(
+    104deg,
+    ${colors.gray['700']}00 15%,
+    ${colors.gray['700']}3F 25%,
+    ${colors.gray['700']}7F 30%,
+    ${colors.gray['700']}FF 48%,
+    ${colors.gray['700']}FF 52%,
+    ${colors.gray['700']}7F 70%,
+    ${colors.gray['700']}3F 75%,
+    ${colors.gray['700']}00 85%
+  );
+  animation: ${pulse} 1.5s linear infinite;
+`

+ 1 - 0
src/shared/components/SkeletonLoader/index.ts

@@ -0,0 +1 @@
+export * from './SkeletonLoader'

+ 5 - 2
src/shared/components/Tabs/Tabs.styles.tsx

@@ -33,6 +33,7 @@ export const TabsGroup = styled.div`
 `
 `
 
 
 export const Tab = styled.div<TabProps>`
 export const Tab = styled.div<TabProps>`
+  transition: border-bottom 0.125s ease, color 0.125s ease;
   width: ${TAB_WIDTH}px;
   width: ${TAB_WIDTH}px;
   min-width: ${TAB_WIDTH}px;
   min-width: ${TAB_WIDTH}px;
   padding: 22px 0;
   padding: 22px 0;
@@ -40,9 +41,11 @@ export const Tab = styled.div<TabProps>`
   color: ${(props) => (props.selected ? colors.white : colors.gray[300])};
   color: ${(props) => (props.selected ? colors.white : colors.gray[300])};
   text-transform: capitalize;
   text-transform: capitalize;
   text-align: center;
   text-align: center;
-  border-bottom: ${(props) => (props.selected ? `4px solid ${colors.blue[500]}` : 'none')};
+  border-bottom: ${(props) => (props.selected ? `4px solid ${colors.blue[500]}` : '4px solid transparent')};
 
 
-  :hover {
+  :hover,
+  :focus {
+    border-bottom: ${(props) => (props.selected ? `4px solid ${colors.blue[500]}` : `4px solid ${colors.gray[300]}`)};
     cursor: pointer;
     cursor: pointer;
   }
   }
 
 

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