Ver código fonte

Release v3.2.0

* 🍎 Public channel balance (#4054)

* Required changes

* Fix balance hook to fetch when there is no active membership

* fixed warning (#4082)

Co-authored-by: Artem <Artem Slugin>

* Setup component

* Setup playground

* Adapt atoms

* Fix story

* Add loading

* Remove channels from navigation

* Remove playground

* Fix channel payouts

* remove channel cta

* Fix ypp for multiple channels #4110

* Fix createAssetUploadEndpoint (#4106)

* remove new & noteworthy page (#4100)

* Display metadata dialog for known extensions (#4112)

* 🍼 Google button on YPP (#4096)

* Google button

* Add google button to channel-already-registered step

* 🈂️ Nft section (#4090)


* Introduce section into view

* Simplify the filter creation logic

* version bump

---------
Co-authored-by: WRadoslaw <r.wyszynski00@gmail.com>
Co-authored-by: drillprop <drylbartosz@gmail.com>
Co-authored-by: Artem <Artem Slugin>
attemka 1 ano atrás
pai
commit
41d2818903
75 arquivos alterados com 1156 adições e 667 exclusões
  1. 16 0
      CHANGELOG.md
  2. 1 1
      packages/atlas/package.json
  3. 3 1
      packages/atlas/src/api/client/cache.ts
  4. 5 0
      packages/atlas/src/api/client/index.ts
  5. 0 21
      packages/atlas/src/api/hooks/channel.ts
  6. 17 95
      packages/atlas/src/api/queries/__generated__/channels.generated.tsx
  7. 1 12
      packages/atlas/src/api/queries/channels.graphql
  8. 79 0
      packages/atlas/src/assets/icons/GoogleSmallLogo.tsx
  9. 18 0
      packages/atlas/src/assets/icons/LogoGoogleWhiteFull.tsx
  10. 24 0
      packages/atlas/src/assets/icons/LogoYoutubeWhiteFull.tsx
  11. 2 0
      packages/atlas/src/assets/icons/index.ts
  12. 15 0
      packages/atlas/src/assets/icons/svgs/logo-google-white-full.svg
  13. 3 0
      packages/atlas/src/assets/icons/svgs/logo-youtube-white-full.svg
  14. 201 0
      packages/atlas/src/components/AllNftSection/AllNftSection.tsx
  15. 1 0
      packages/atlas/src/components/AllNftSection/index.ts
  16. 12 3
      packages/atlas/src/components/FilterButton/FilterButton.stories.tsx
  17. 1 1
      packages/atlas/src/components/FilterButton/FilterButton.styles.ts
  18. 142 64
      packages/atlas/src/components/FilterButton/FilterButton.tsx
  19. 21 1
      packages/atlas/src/components/MobileFilterButton/MobileFilterButton.tsx
  20. 1 0
      packages/atlas/src/components/Section/Section.stories.tsx
  21. 1 1
      packages/atlas/src/components/Section/SectionContent/SectionContent.styles.ts
  22. 1 0
      packages/atlas/src/components/Section/SectionFooter/SectionFooter.stories.tsx
  23. 8 1
      packages/atlas/src/components/Section/SectionFooter/SectionFooter.tsx
  24. 9 4
      packages/atlas/src/components/Section/SectionHeader/SectionFilters/SectionFilters.tsx
  25. 4 2
      packages/atlas/src/components/Section/SectionHeader/SectionHeader.tsx
  26. 3 1
      packages/atlas/src/components/Section/SectionHeader/SectionTitle/SectionTitle.tsx
  27. 1 1
      packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.stories.tsx
  28. 11 5
      packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.styles.ts
  29. 0 15
      packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.tsx
  30. 61 0
      packages/atlas/src/components/_buttons/GoogleButton/GoogleButton.tsx
  31. 1 0
      packages/atlas/src/components/_buttons/GoogleButton/index.ts
  32. 17 7
      packages/atlas/src/components/_channel/ChannelCard/ChannelCard.stories.tsx
  33. 45 0
      packages/atlas/src/components/_channel/ChannelsSection/ChannelsSection.stories.tsx
  34. 69 0
      packages/atlas/src/components/_channel/ChannelsSection/ChannelsSection.tsx
  35. 1 0
      packages/atlas/src/components/_channel/ChannelsSection/index.ts
  36. 2 16
      packages/atlas/src/components/_channel/ExpandableChannelsList/ExpandableChannelsList.tsx
  37. 48 0
      packages/atlas/src/components/_inputs/PriceRangeInput/PriceRangeInput.tsx
  38. 1 0
      packages/atlas/src/components/_inputs/PriceRangeInput/index.ts
  39. 7 0
      packages/atlas/src/components/_inputs/ToggleButtonGroup/ToggleButtonGroup.styles.ts
  40. 3 3
      packages/atlas/src/components/_inputs/ToggleButtonGroup/ToggleButtonGroup.tsx
  41. 0 15
      packages/atlas/src/components/_navigation/SidenavViewer/SidenavViewer.tsx
  42. 9 6
      packages/atlas/src/components/_overlays/Dialog/Dialog.tsx
  43. 1 1
      packages/atlas/src/components/_overlays/DialogModal/DialogModal.stories.tsx
  44. 1 1
      packages/atlas/src/components/_templates/VideoContentTemplate.tsx
  45. 0 1
      packages/atlas/src/config/routes.ts
  46. 1 1
      packages/atlas/src/index.html
  47. 2 2
      packages/atlas/src/providers/joystream/joystream.hooks.ts
  48. 5 4
      packages/atlas/src/providers/transactions/transactions.hooks.ts
  49. 1 1
      packages/atlas/src/providers/uploads/uploads.hooks.ts
  50. 1 1
      packages/atlas/src/types/cta.ts
  51. 27 0
      packages/atlas/src/utils/asset.test.ts
  52. 10 9
      packages/atlas/src/utils/asset.ts
  53. 8 34
      packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.styles.ts
  54. 64 74
      packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx
  55. 1 1
      packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationRequirementsStep/YppAuthorizationRequirementsStep.styles.ts
  56. 17 10
      packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationRequirementsStep/YppAuthorizationRequirementsStep.tsx
  57. 2 19
      packages/atlas/src/views/global/YppLandingView/YppFooter.styles.ts
  58. 6 8
      packages/atlas/src/views/global/YppLandingView/YppFooter.tsx
  59. 8 0
      packages/atlas/src/views/global/YppLandingView/YppHero.styles.ts
  60. 20 22
      packages/atlas/src/views/global/YppLandingView/YppHero.tsx
  61. 25 14
      packages/atlas/src/views/global/YppLandingView/YppThreeStepsSection.tsx
  62. 1 1
      packages/atlas/src/views/playground/PlaygroundLayout.tsx
  63. 0 1
      packages/atlas/src/views/playground/Playgrounds/index.ts
  64. 7 1
      packages/atlas/src/views/studio/MyPaymentsView/PaymentsOverview/PaymentsOverview.hooks.ts
  65. 1 1
      packages/atlas/src/views/viewer/CategoryView/CategoryView.tsx
  66. 37 0
      packages/atlas/src/views/viewer/ChannelView/ChannelView.styles.ts
  67. 35 8
      packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx
  68. 3 1
      packages/atlas/src/views/viewer/ChannelsView/ChannelsView.tsx
  69. 1 1
      packages/atlas/src/views/viewer/HomeView.tsx
  70. 0 23
      packages/atlas/src/views/viewer/NewView/NewView.tsx
  71. 0 1
      packages/atlas/src/views/viewer/NewView/index.ts
  72. 3 143
      packages/atlas/src/views/viewer/NftsView/NftsView.tsx
  73. 1 3
      packages/atlas/src/views/viewer/PopularView/PopularView.tsx
  74. 2 2
      packages/atlas/src/views/viewer/VideoView/VideoView.tsx
  75. 0 2
      packages/atlas/src/views/viewer/ViewerLayout.tsx

+ 16 - 0
CHANGELOG.md

@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [3.2.0] - 2023-04-21
+
+### Changed
+
+- NFT section rework
+- YPP sign-up buttons changed according to Google guidelines
+- Channels section removed from navigation
+- New & noteworthy pages were removed
+- Metadata update for supported wallets only
+
+### Fixed
+
+- Fixed bug with channel payouts
+- Fixed parsing of asset URL
+- Fixed YPP opt-in for memberships with multiple channels
+
 ## [3.1.0] - 2023-04-14
 
 ### Added

+ 1 - 1
packages/atlas/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@joystream/atlas",
   "description": "UI for consuming Joystream - a user governed video platform",
-  "version": "3.1.0",
+  "version": "3.2.0",
   "license": "GPL-3.0",
   "scripts": {
     "start": "vite",

+ 3 - 1
packages/atlas/src/api/client/cache.ts

@@ -70,9 +70,11 @@ const getNftKeyArgs = (
   const sorting = stringifyValue(sortingArray)
   const createdAtGte = args?.where?.createdAt_gte ? JSON.stringify(args.where.createdAt_gte) : ''
   const createdAtLte = args?.where?.createdAt_lte ? JSON.stringify(args.where.createdAt_lte) : ''
+  const lastSalePriceGte = args?.where?.lastSalePrice_gte ? JSON.stringify(args.where.lastSalePrice_gte) : ''
+  const lastSalePriceLte = args?.where?.lastSalePrice_lte ? JSON.stringify(args.where.lastSalePrice_lte) : ''
   const video = stringifyValue(args?.where?.video)
 
-  return `${OR}:${AND}:${ownerMember}:${creatorChannel}:${status}:${auctionStatus}:${sorting}:${createdAtGte}:${createdAtLte}:${video}:${offset}`
+  return `${OR}:${AND}:${ownerMember}:${creatorChannel}:${status}:${auctionStatus}:${sorting}:${createdAtGte}:${createdAtLte}:${video}:${offset}:${lastSalePriceGte}:${lastSalePriceLte}`
 }
 
 const getChannelKeyArgs = (args: Partial<QueryChannelsConnectionArgs> | null) => {

+ 5 - 0
packages/atlas/src/api/client/index.ts

@@ -88,6 +88,11 @@ const createApolloClient = () => {
             if (!parent.type) {
               return null
             }
+
+            if (parent.type.__typename === 'DataObjectTypeChannelPayoutsPayload') {
+              // if this is a payload file skip testing and just return first url.
+              return resolvedUrls[0]
+            }
             const distributorUrl = resolvedUrl.split(`/${atlasConfig.storage.assetPath}/${parent.id}`)[0]
 
             const assetTestPromise = testAssetDownload(resolvedUrl, parent.type)

+ 0 - 21
packages/atlas/src/api/hooks/channel.ts

@@ -14,8 +14,6 @@ import {
   GetExtendedFullChannelsQueryVariables,
   GetPopularChannelsQuery,
   GetPopularChannelsQueryVariables,
-  GetPromisingChannelsQuery,
-  GetPromisingChannelsQueryVariables,
   GetTop10ChannelsQuery,
   GetTop10ChannelsQueryVariables,
   UnfollowChannelMutation,
@@ -25,7 +23,6 @@ import {
   useGetExtendedBasicChannelsQuery,
   useGetExtendedFullChannelsQuery,
   useGetPopularChannelsQuery,
-  useGetPromisingChannelsQuery,
   useGetTop10ChannelsQuery,
   useUnfollowChannelMutation,
 } from '@/api/queries/__generated__/channels.generated'
@@ -193,24 +190,6 @@ export const useDiscoverChannels = (
   }
 }
 
-export const usePromisingChannels = (
-  variables?: GetPromisingChannelsQueryVariables,
-  opts?: QueryHookOptions<GetPromisingChannelsQuery, GetPromisingChannelsQueryVariables>
-) => {
-  const { data, ...rest } = useGetPromisingChannelsQuery({
-    ...opts,
-    variables,
-  })
-
-  const shuffledChannels = useShuffleResults<GetDiscoverChannelsQuery['mostRecentChannels'][number]>(
-    data?.mostRecentChannels
-  )
-  return {
-    extendedChannels: shuffledChannels,
-    ...rest,
-  }
-}
-
 export const usePopularChannels = (
   variables?: GetPopularChannelsQueryVariables,
   opts?: QueryHookOptions<GetPopularChannelsQuery, GetPopularChannelsQueryVariables>

+ 17 - 95
packages/atlas/src/api/queries/__generated__/channels.generated.tsx

@@ -8,6 +8,7 @@ import {
   ExtendedBasicChannelFieldsFragmentDoc,
   ExtendedFullChannelFieldsFragmentDoc,
   FullChannelFieldsFragmentDoc,
+  StorageDataObjectFieldsFragmentDoc,
 } from './fragments.generated'
 
 const defaultOptions = {} as const
@@ -358,46 +359,6 @@ export type GetTop10ChannelsQuery = {
   }>
 }
 
-export type GetPromisingChannelsQueryVariables = Types.Exact<{
-  where?: Types.InputMaybe<Types.ExtendedChannelWhereInput>
-}>
-
-export type GetPromisingChannelsQuery = {
-  __typename?: 'Query'
-  mostRecentChannels: Array<{
-    __typename?: 'ExtendedChannel'
-    channel: {
-      __typename?: 'Channel'
-      id: string
-      title?: string | null
-      description?: string | null
-      createdAt: Date
-      followsNum: number
-      rewardAccount: string
-      channelStateBloatBond: string
-      avatarPhoto?: {
-        __typename?: 'StorageDataObject'
-        id: string
-        resolvedUrls: Array<string>
-        resolvedUrl?: string | null
-        createdAt: Date
-        size: string
-        isAccepted: boolean
-        ipfsHash: string
-        storageBag: { __typename?: 'StorageBag'; id: string }
-        type?:
-          | { __typename: 'DataObjectTypeChannelAvatar' }
-          | { __typename: 'DataObjectTypeChannelCoverPhoto' }
-          | { __typename: 'DataObjectTypeChannelPayoutsPayload' }
-          | { __typename: 'DataObjectTypeVideoMedia' }
-          | { __typename: 'DataObjectTypeVideoSubtitle' }
-          | { __typename: 'DataObjectTypeVideoThumbnail' }
-          | null
-      } | null
-    }
-  }>
-}
-
 export type GetDiscoverChannelsQueryVariables = Types.Exact<{
   where?: Types.InputMaybe<Types.ExtendedChannelWhereInput>
 }>
@@ -570,9 +531,22 @@ export type GetPayloadDataByCommitmentQuery = {
           __typename?: 'ChannelPayoutsUpdatedEventData'
           payloadDataObject?: {
             __typename?: 'StorageDataObject'
-            isAccepted: boolean
+            id: string
             resolvedUrls: Array<string>
             resolvedUrl?: string | null
+            createdAt: Date
+            size: string
+            isAccepted: boolean
+            ipfsHash: string
+            storageBag: { __typename?: 'StorageBag'; id: string }
+            type?:
+              | { __typename: 'DataObjectTypeChannelAvatar' }
+              | { __typename: 'DataObjectTypeChannelCoverPhoto' }
+              | { __typename: 'DataObjectTypeChannelPayoutsPayload' }
+              | { __typename: 'DataObjectTypeVideoMedia' }
+              | { __typename: 'DataObjectTypeVideoSubtitle' }
+              | { __typename: 'DataObjectTypeVideoThumbnail' }
+              | null
           } | null
         }
       | { __typename?: 'ChannelRewardClaimedAndWithdrawnEventData' }
@@ -1031,57 +1005,6 @@ export function useGetTop10ChannelsLazyQuery(
 export type GetTop10ChannelsQueryHookResult = ReturnType<typeof useGetTop10ChannelsQuery>
 export type GetTop10ChannelsLazyQueryHookResult = ReturnType<typeof useGetTop10ChannelsLazyQuery>
 export type GetTop10ChannelsQueryResult = Apollo.QueryResult<GetTop10ChannelsQuery, GetTop10ChannelsQueryVariables>
-export const GetPromisingChannelsDocument = gql`
-  query GetPromisingChannels($where: ExtendedChannelWhereInput) {
-    mostRecentChannels(where: $where, orderBy: videoViewsNum_DESC, mostRecentLimit: 100, resultsLimit: 15) {
-      channel {
-        ...BasicChannelFields
-      }
-    }
-  }
-  ${BasicChannelFieldsFragmentDoc}
-`
-
-/**
- * __useGetPromisingChannelsQuery__
- *
- * To run a query within a React component, call `useGetPromisingChannelsQuery` and pass it any options that fit your needs.
- * When your component renders, `useGetPromisingChannelsQuery` 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 } = useGetPromisingChannelsQuery({
- *   variables: {
- *      where: // value for 'where'
- *   },
- * });
- */
-export function useGetPromisingChannelsQuery(
-  baseOptions?: Apollo.QueryHookOptions<GetPromisingChannelsQuery, GetPromisingChannelsQueryVariables>
-) {
-  const options = { ...defaultOptions, ...baseOptions }
-  return Apollo.useQuery<GetPromisingChannelsQuery, GetPromisingChannelsQueryVariables>(
-    GetPromisingChannelsDocument,
-    options
-  )
-}
-export function useGetPromisingChannelsLazyQuery(
-  baseOptions?: Apollo.LazyQueryHookOptions<GetPromisingChannelsQuery, GetPromisingChannelsQueryVariables>
-) {
-  const options = { ...defaultOptions, ...baseOptions }
-  return Apollo.useLazyQuery<GetPromisingChannelsQuery, GetPromisingChannelsQueryVariables>(
-    GetPromisingChannelsDocument,
-    options
-  )
-}
-export type GetPromisingChannelsQueryHookResult = ReturnType<typeof useGetPromisingChannelsQuery>
-export type GetPromisingChannelsLazyQueryHookResult = ReturnType<typeof useGetPromisingChannelsLazyQuery>
-export type GetPromisingChannelsQueryResult = Apollo.QueryResult<
-  GetPromisingChannelsQuery,
-  GetPromisingChannelsQueryVariables
->
 export const GetDiscoverChannelsDocument = gql`
   query GetDiscoverChannels($where: ExtendedChannelWhereInput) {
     mostRecentChannels(where: $where, orderBy: followsNum_DESC, mostRecentLimit: 100, resultsLimit: 15) {
@@ -1340,14 +1263,13 @@ export const GetPayloadDataByCommitmentDocument = gql`
       data {
         ... on ChannelPayoutsUpdatedEventData {
           payloadDataObject {
-            isAccepted
-            resolvedUrls
-            resolvedUrl @client
+            ...StorageDataObjectFields
           }
         }
       }
     }
   }
+  ${StorageDataObjectFieldsFragmentDoc}
 `
 
 /**

+ 1 - 12
packages/atlas/src/api/queries/channels.graphql

@@ -81,15 +81,6 @@ query GetTop10Channels($where: ExtendedChannelWhereInput) {
   }
 }
 
-query GetPromisingChannels($where: ExtendedChannelWhereInput) {
-  # CHANGE: Replacement for overly-specific `promisingChannels` query
-  mostRecentChannels(where: $where, orderBy: videoViewsNum_DESC, mostRecentLimit: 100, resultsLimit: 15) {
-    channel {
-      ...BasicChannelFields
-    }
-  }
-}
-
 query GetDiscoverChannels($where: ExtendedChannelWhereInput) {
   # CHANGE: Replacement for overly-specific `discoverChannels` query
   mostRecentChannels(where: $where, orderBy: followsNum_DESC, mostRecentLimit: 100, resultsLimit: 15) {
@@ -140,9 +131,7 @@ query GetPayloadDataByCommitment($commitment: String!) {
     data {
       ... on ChannelPayoutsUpdatedEventData {
         payloadDataObject {
-          isAccepted
-          resolvedUrls
-          resolvedUrl @client
+          ...StorageDataObjectFields
         }
       }
     }

+ 79 - 0
packages/atlas/src/assets/icons/GoogleSmallLogo.tsx

@@ -0,0 +1,79 @@
+import { Ref, SVGProps, forwardRef, memo } from 'react'
+
+const GoogleSmallLogo = forwardRef((props: SVGProps<SVGSVGElement>, ref: Ref<SVGSVGElement>) => (
+  <svg xmlns="http://www.w3.org/2000/svg" width="46px" height="46px" viewBox="0 0 46 46" ref={ref} {...props}>
+    <defs>
+      <filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
+        <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
+        <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
+        <feColorMatrix
+          values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.168 0"
+          in="shadowBlurOuter1"
+          type="matrix"
+          result="shadowMatrixOuter1"
+        />
+        <feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2" />
+        <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter2" result="shadowBlurOuter2" />
+        <feColorMatrix
+          values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.084 0"
+          in="shadowBlurOuter2"
+          type="matrix"
+          result="shadowMatrixOuter2"
+        />
+        <feMerge>
+          <feMergeNode in="shadowMatrixOuter1" />
+          <feMergeNode in="shadowMatrixOuter2" />
+          <feMergeNode in="SourceGraphic" />
+        </feMerge>
+      </filter>
+      <rect id="path-2" x="0" y="0" width="40" height="40" rx="2" />
+      <rect id="path-3" x="5" y="5" width="38" height="38" rx="1" />
+    </defs>
+    <g id="Google-Button" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
+      <g id="9-PATCH" transform="translate(-608.000000, -219.000000)" />
+      <g id="btn_google_dark_normal" transform="translate(-1.000000, -1.000000)">
+        <g id="button" transform="translate(4.000000, 4.000000)" filter="url(#filter-1)">
+          <g id="button-bg">
+            <use fill="#4285F4" fillRule="evenodd" />
+            <use fill="none" />
+            <use fill="none" />
+            <use fill="none" />
+          </g>
+        </g>
+        <g id="button-bg-copy">
+          <use fill="#FFFFFF" fillRule="evenodd" />
+          <use fill="none" />
+          <use fill="none" />
+          <use fill="none" />
+        </g>
+        <g id="logo_googleg_48dp" transform="translate(15.000000, 15.000000)">
+          <path
+            d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z"
+            id="Shape"
+            fill="#4285F4"
+          />
+          <path
+            d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z"
+            id="Shape"
+            fill="#34A853"
+          />
+          <path
+            d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z"
+            id="Shape"
+            fill="#FBBC05"
+          />
+          <path
+            d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z"
+            id="Shape"
+            fill="#EA4335"
+          />
+          <path d="M0,0 L18,0 L18,18 L0,18 L0,0 Z" id="Shape" />
+        </g>
+        <g id="handles_square" />
+      </g>
+    </g>
+  </svg>
+))
+GoogleSmallLogo.displayName = 'GoogleSmallLogo'
+const Memo = memo(GoogleSmallLogo)
+export { Memo as SvgGoogleSmallLogo }

+ 18 - 0
packages/atlas/src/assets/icons/LogoGoogleWhiteFull.tsx

@@ -0,0 +1,18 @@
+// THIS FILE WAS AUTOGENERATED BY SVGR. DO NOT MODIFY IT MANUALLY;
+import { Ref, SVGProps, forwardRef, memo } from 'react'
+
+const SvgLogoGoogleWhiteFull = forwardRef((props: SVGProps<SVGSVGElement>, ref: Ref<SVGSVGElement>) => (
+  <svg width={108} height={36} viewBox="0 0 108 36" fill="none" xmlns="http://www.w3.org/2000/svg" ref={ref} {...props}>
+    <g clipPath="url(#clip0_2208_4389)" fill="#fff">
+      <path d="M26.377 12.446h-12.52v3.715h8.88c-.44 5.2-4.773 7.432-8.865 7.432a9.76 9.76 0 0 1-9.802-9.891c0-5.624 4.354-9.954 9.814-9.954 4.212 0 6.694 2.685 6.694 2.685l2.6-2.694S19.838.022 13.748.022C5.993.022-.006 6.567-.006 13.636c0 6.927 5.643 13.682 13.95 13.682 7.307 0 12.656-5.006 12.656-12.408 0-1.562-.227-2.464-.227-2.464h.004ZM36.634 9.755c-5.138 0-8.82 4.017-8.82 8.7 0 4.754 3.57 8.845 8.88 8.845 4.806 0 8.743-3.673 8.743-8.743 0-5.8-4.58-8.802-8.803-8.802Zm.05 3.446c2.526 0 4.92 2.043 4.92 5.334 0 3.22-2.384 5.322-4.932 5.322-2.8 0-5-2.242-5-5.348 0-3.04 2.18-5.308 5.02-5.308h-.008ZM55.815 9.755c-5.138 0-8.82 4.017-8.82 8.7 0 4.754 3.57 8.845 8.88 8.845 4.806 0 8.743-3.673 8.743-8.743 0-5.8-4.58-8.802-8.803-8.802Zm.05 3.446c2.526 0 4.92 2.043 4.92 5.334 0 3.22-2.384 5.322-4.932 5.322-2.8 0-5-2.242-5-5.348 0-3.04 2.18-5.308 5.02-5.308h-.008ZM74.628 9.765c-4.716 0-8.422 4.13-8.422 8.766 0 5.28 4.297 8.782 8.34 8.782 2.5 0 3.83-.993 4.8-2.132v1.73c0 3.027-1.838 4.84-4.612 4.84-2.68 0-4.024-1.993-4.5-3.123l-3.372 1.4c1.196 2.53 3.604 5.167 7.9 5.167 4.7 0 8.262-2.953 8.262-9.147V10.292H79.36v1.486c-1.13-1.22-2.678-2.013-4.73-2.013h-.002Zm.34 3.44c2.312 0 4.686 1.974 4.686 5.345 0 3.427-2.37 5.315-4.737 5.315-2.514 0-4.853-2.04-4.853-5.283 0-3.368 2.43-5.377 4.904-5.377ZM99.4 9.744c-4.448 0-8.183 3.54-8.183 8.76 0 5.526 4.163 8.803 8.6 8.803 3.712 0 6-2.03 7.35-3.85l-3.033-2.018c-.787 1.22-2.103 2.415-4.298 2.415-2.466 0-3.6-1.35-4.303-2.66l11.763-4.88-.6-1.43c-1.136-2.8-3.788-5.14-7.296-5.14Zm.153 3.374c1.603 0 2.756.852 3.246 1.874l-7.856 3.283c-.34-2.542 2.07-5.157 4.6-5.157h.01ZM85.6 26.787h3.864V.93H85.6v25.857Z" />
+    </g>
+    <defs>
+      <clipPath id="clip0_2208_4389">
+        <path fill="#fff" d="M0 0h108v36H0z" />
+      </clipPath>
+    </defs>
+  </svg>
+))
+SvgLogoGoogleWhiteFull.displayName = 'SvgLogoGoogleWhiteFull'
+const Memo = memo(SvgLogoGoogleWhiteFull)
+export { Memo as SvgLogoGoogleWhiteFull }

+ 24 - 0
packages/atlas/src/assets/icons/LogoYoutubeWhiteFull.tsx

@@ -0,0 +1,24 @@
+// THIS FILE WAS AUTOGENERATED BY SVGR. DO NOT MODIFY IT MANUALLY;
+import { Ref, SVGProps, forwardRef, memo } from 'react'
+
+const SvgLogoYoutubeWhiteFull = forwardRef((props: SVGProps<SVGSVGElement>, ref: Ref<SVGSVGElement>) => (
+  <svg width={110} height={24} viewBox="0 0 110 24" fill="none" xmlns="http://www.w3.org/2000/svg" ref={ref} {...props}>
+    <g clipPath="url(#clip0_2208_4388)" fill="#fff">
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M31.395.522a4.39 4.39 0 0 1 3.088 3.088c.738 2.73.76 8.39.76 8.39s0 5.683-.738 8.39a4.39 4.39 0 0 1-3.088 3.088c-2.707.738-13.604.738-13.604.738s-10.896 0-13.603-.738a4.39 4.39 0 0 1-3.088-3.088C.384 17.66.384 12 .384 12s0-5.66.716-8.368A4.39 4.39 0 0 1 4.187.544c2.708-.738 13.604-.76 13.604-.76s10.896 0 13.604.738ZM14.323 6.764 23.363 12l-9.04 5.236V6.764Z"
+      />
+      <path d="M50.95 22.001c-.693-.47-1.186-1.185-1.477-2.17-.29-.984-.425-2.282-.425-3.915V13.7c0-1.633.157-2.975.493-3.96.335-1.007.85-1.723 1.566-2.17.716-.448 1.633-.694 2.774-.694 1.12 0 2.036.224 2.708.694.67.47 1.186 1.186 1.499 2.17.313.985.47 2.305.47 3.938v2.215c0 1.633-.157 2.931-.47 3.916-.313.984-.806 1.7-1.5 2.17-.693.448-1.633.694-2.796.694-1.208.022-2.148-.224-2.842-.672Zm3.871-2.394c.201-.492.29-1.32.29-2.438v-4.766c0-1.097-.089-1.902-.29-2.394-.201-.515-.537-.761-1.007-.761-.47 0-.783.246-.984.76-.202.515-.291 1.298-.291 2.395v4.766c0 1.118.09 1.946.268 2.438.18.493.515.739 1.007.739.47 0 .806-.246 1.007-.739Zm49.045-2.864.09 2.216c.067.492.179.85.358 1.073.179.224.47.336.85.336.515 0 .873-.201 1.052-.604.201-.403.29-1.074.313-1.991l2.976.179c.022.134.022.313.022.537 0 1.41-.38 2.46-1.164 3.154-.783.694-1.857 1.052-3.266 1.052-1.701 0-2.887-.537-3.558-1.589-.671-1.051-1.029-2.707-1.029-4.922v-2.707c0-2.283.358-3.96 1.052-5.012.716-1.052 1.924-1.589 3.624-1.589 1.186 0 2.081.224 2.708.65.626.424 1.074 1.095 1.342 2.013.269.917.38 2.17.38 3.781v2.618h-5.75v.805Zm.448-7.227c-.179.224-.291.56-.358 1.052s-.09 1.23-.09 2.238v1.096h2.506v-1.096c0-.985-.022-1.723-.089-2.238-.067-.515-.179-.873-.358-1.074-.179-.201-.448-.313-.806-.313-.38.022-.649.134-.805.335ZM42.582 15.67 38.667 1.53h3.423l1.365 6.399c.358 1.588.604 2.93.783 4.027h.09c.111-.805.38-2.125.782-4.005l1.41-6.421h3.423l-3.96 14.118v6.78h-3.378v-6.758h-.023ZM69.655 7.167V22.45h-2.684l-.291-1.88h-.068c-.738 1.41-1.834 2.126-3.289 2.126-1.006 0-1.767-.336-2.237-1.007-.492-.671-.716-1.7-.716-3.11V7.168h3.446v11.209c0 .671.067 1.163.223 1.454.157.291.403.425.739.425.29 0 .581-.09.85-.268.269-.18.47-.425.604-.694V7.167h3.423ZM87.309 7.167V22.45h-2.685l-.29-1.88h-.068c-.738 1.41-1.835 2.126-3.289 2.126-1.007 0-1.768-.336-2.237-1.007-.493-.671-.716-1.7-.716-3.11V7.168h3.445v11.209c0 .671.067 1.163.224 1.454.157.291.403.425.738.425.291 0 .582-.09.85-.268.27-.18.47-.425.605-.694V7.167h3.423Z" />
+      <path d="M79.008 4.303h-3.423V22.45h-3.356V4.303h-3.424V1.53h10.18v2.774h.023Zm19.667 5.303c-.201-.962-.537-1.656-1.007-2.103-.47-.425-1.096-.65-1.924-.65-.626 0-1.23.18-1.767.538-.56.358-.963.828-1.276 1.41h-.022V.745h-3.311v21.68h2.841l.358-1.454h.067c.269.515.671.918 1.186 1.23.542.298 1.15.452 1.768.448 1.163 0 2.013-.537 2.573-1.61.537-1.075.828-2.753.828-5.013v-2.416c0-1.7-.112-3.043-.314-4.005Zm-3.154 6.242c0 1.12-.045 1.992-.135 2.618-.09.627-.246 1.074-.47 1.343a1.083 1.083 0 0 1-.872.402c-.291 0-.537-.067-.783-.2-.246-.135-.425-.337-.582-.605v-8.681c.112-.425.313-.761.604-1.03.29-.268.582-.402.917-.402.358 0 .627.134.806.402.201.269.313.739.403 1.388.067.649.111 1.566.111 2.774v1.991Z" />
+    </g>
+    <defs>
+      <clipPath id="clip0_2208_4388">
+        <path fill="#fff" d="M0 0h110v24H0z" />
+      </clipPath>
+    </defs>
+  </svg>
+))
+SvgLogoYoutubeWhiteFull.displayName = 'SvgLogoYoutubeWhiteFull'
+const Memo = memo(SvgLogoYoutubeWhiteFull)
+export { Memo as SvgLogoYoutubeWhiteFull }

+ 2 - 0
packages/atlas/src/assets/icons/index.ts

@@ -206,6 +206,7 @@ export * from './LogoFacebookOnLight'
 export * from './LogoGithubMonochrome'
 export * from './LogoGithubOnDark'
 export * from './LogoGithubOnLight'
+export * from './LogoGoogleWhiteFull'
 export * from './LogoInstagramMonochrome'
 export * from './LogoInstagramOnDark'
 export * from './LogoInstagramOnLight'
@@ -235,6 +236,7 @@ export * from './LogoVkOnLight'
 export * from './LogoWhatsAppMonochrome'
 export * from './LogoWhatsAppOnDark'
 export * from './LogoWhatsAppOnLight'
+export * from './LogoYoutubeWhiteFull'
 export * from './LogoYoutube'
 export * from './SidebarChannel'
 export * from './SidebarChannels'

+ 15 - 0
packages/atlas/src/assets/icons/svgs/logo-google-white-full.svg

@@ -0,0 +1,15 @@
+<svg width="108" height="36" viewBox="0 0 108 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2208_4389)">
+<path d="M26.377 12.446H13.857V16.161H22.737C22.297 21.361 17.964 23.593 13.872 23.593C12.5757 23.5987 11.2912 23.3461 10.0936 22.8499C8.89595 22.3537 7.80916 21.624 6.89666 20.7032C5.98416 19.7824 5.26423 18.689 4.77891 17.487C4.29359 16.2849 4.05261 14.9982 4.07002 13.702C4.07002 8.07797 8.42402 3.74797 13.884 3.74797C18.096 3.74797 20.578 6.43297 20.578 6.43297L23.178 3.73897C23.178 3.73897 19.838 0.0219727 13.748 0.0219727C5.99302 0.0219727 -0.00598145 6.56697 -0.00598145 13.636C-0.00598145 20.563 5.63702 27.318 13.944 27.318C21.251 27.318 26.6 22.312 26.6 14.91C26.6 13.348 26.373 12.446 26.373 12.446H26.377Z" fill="white"/>
+<path d="M36.634 9.75503C31.496 9.75503 27.814 13.772 27.814 18.455C27.814 23.209 31.384 27.3 36.694 27.3C41.5 27.3 45.437 23.627 45.437 18.557C45.437 12.757 40.857 9.75503 36.634 9.75503ZM36.684 13.201C39.21 13.201 41.604 15.244 41.604 18.535C41.604 21.755 39.22 23.857 36.672 23.857C33.872 23.857 31.672 21.615 31.672 18.509C31.672 15.469 33.852 13.201 36.692 13.201H36.684Z" fill="white"/>
+<path d="M55.815 9.75503C50.677 9.75503 46.995 13.772 46.995 18.455C46.995 23.209 50.565 27.3 55.875 27.3C60.681 27.3 64.618 23.627 64.618 18.557C64.618 12.757 60.038 9.75503 55.815 9.75503ZM55.865 13.201C58.391 13.201 60.785 15.244 60.785 18.535C60.785 21.755 58.401 23.857 55.853 23.857C53.053 23.857 50.853 21.615 50.853 18.509C50.853 15.469 53.033 13.201 55.873 13.201H55.865Z" fill="white"/>
+<path d="M74.6281 9.76501C69.912 9.76501 66.2061 13.895 66.2061 18.531C66.2061 23.811 70.5031 27.313 74.5461 27.313C77.0461 27.313 78.3761 26.32 79.3461 25.181V26.911C79.3461 29.938 77.5081 31.751 74.7341 31.751C72.0541 31.751 70.7101 29.758 70.2341 28.628L66.8621 30.028C68.0581 32.558 70.4661 35.195 74.7621 35.195C79.4621 35.195 83.0241 32.242 83.0241 26.048V10.292H79.3601V11.778C78.2301 10.558 76.6821 9.76501 74.6301 9.76501H74.6281ZM74.9681 13.205C77.2801 13.205 79.6541 15.179 79.6541 18.55C79.6541 21.977 77.284 23.865 74.9171 23.865C72.4031 23.865 70.0641 21.825 70.0641 18.582C70.0641 15.214 72.4941 13.205 74.9681 13.205Z" fill="white"/>
+<path d="M99.4 9.74402C94.952 9.74402 91.217 13.284 91.217 18.504C91.217 24.03 95.38 27.307 99.817 27.307C103.529 27.307 105.817 25.277 107.167 23.457L104.134 21.439C103.347 22.659 102.031 23.854 99.836 23.854C97.37 23.854 96.236 22.504 95.533 21.194L107.296 16.314L106.696 14.884C105.56 12.084 102.908 9.74402 99.4 9.74402ZM99.553 13.118C101.156 13.118 102.309 13.97 102.799 14.992L94.943 18.275C94.603 15.733 97.013 13.118 99.543 13.118H99.553Z" fill="white"/>
+<path d="M85.6 26.787H89.464V0.929993H85.6V26.787Z" fill="white"/>
+</g>
+<defs>
+<clipPath id="clip0_2208_4389">
+<rect width="108" height="36" fill="white"/>
+</clipPath>
+</defs>
+</svg>

Diferenças do arquivo suprimidas por serem muito extensas
+ 3 - 0
packages/atlas/src/assets/icons/svgs/logo-youtube-white-full.svg


+ 201 - 0
packages/atlas/src/components/AllNftSection/AllNftSection.tsx

@@ -0,0 +1,201 @@
+import styled from '@emotion/styled'
+import { useMemo, useState } from 'react'
+
+import { useNftsConnection } from '@/api/hooks/nfts'
+import { OwnedNftOrderByInput, OwnedNftWhereInput } from '@/api/queries/__generated__/baseTypes.generated'
+import { SvgActionSell, SvgActionSettings, SvgActionShoppingCart } from '@/assets/icons'
+import { EmptyFallback } from '@/components/EmptyFallback'
+import { FilterButtonOption, SectionFilter } from '@/components/FilterButton'
+import { NumberFormat } from '@/components/NumberFormat'
+import { Section } from '@/components/Section/Section'
+import { Button } from '@/components/_buttons/Button'
+import { NftTileViewer } from '@/components/_nft/NftTileViewer'
+import { useMediaMatch } from '@/hooks/useMediaMatch'
+import { tokenNumberToHapiBn } from '@/joystream-lib/utils'
+import { createPlaceholderData } from '@/utils/data'
+
+const NFT_STATUSES: FilterButtonOption[] = [
+  {
+    value: 'AuctionTypeEnglish',
+    selected: false,
+    applied: false,
+    label: 'Timed auction',
+  },
+  {
+    value: 'AuctionTypeOpen',
+    selected: false,
+    applied: false,
+    label: 'Open auction',
+  },
+  {
+    value: 'TransactionalStatusBuyNow',
+    selected: false,
+    applied: false,
+    label: 'Fixed price',
+  },
+  {
+    value: 'TransactionalStatusIdle',
+    selected: false,
+    applied: false,
+    label: 'Not for sale',
+  },
+]
+
+const OTHER: FilterButtonOption[] = [
+  { label: 'Exclude paid promotional materials', selected: false, applied: false, value: 'promotional' },
+  { label: 'Exclude mature content rating', selected: false, applied: false, value: 'mature' },
+]
+
+const FILTERS: SectionFilter[] = [
+  {
+    name: 'price',
+    type: 'range',
+    label: 'Last price',
+    icon: <SvgActionSell />,
+    range: { min: undefined, max: undefined },
+  },
+  {
+    name: 'status',
+    label: 'Status',
+    icon: <SvgActionShoppingCart />,
+    type: 'checkbox',
+    options: NFT_STATUSES,
+  },
+  { name: 'other', type: 'checkbox', options: OTHER, label: 'Other', icon: <SvgActionSettings /> },
+]
+
+const LIMIT = 12
+const LG_LIMIT = 30
+
+export const AllNftSection = () => {
+  const [filters, setFilters] = useState<SectionFilter[]>(FILTERS)
+  const [hasAppliedFilters, setHasAppliedFilters] = useState(false)
+  const [order, setOrder] = useState<OwnedNftOrderByInput>(OwnedNftOrderByInput.CreatedAtDesc)
+  const smMatch = useMediaMatch('sm')
+  const lgMatch = useMediaMatch('lg')
+  const limit = lgMatch ? LG_LIMIT : LIMIT
+  const mappedFilters = useMemo((): OwnedNftWhereInput => {
+    const mappedStatus =
+      filters
+        .find((filter) => filter.name === 'status')
+        ?.options?.filter((option) => option.applied)
+        .map((option) => {
+          if (['AuctionTypeOpen', 'AuctionTypeEnglish'].includes(option.value)) {
+            return {
+              auction: {
+                auctionType: {
+                  isTypeOf_eq: option.value,
+                },
+              },
+            }
+          }
+
+          return { isTypeOf_eq: option.value }
+        }, [] as OwnedNftWhereInput['transactionalStatus'][]) ?? []
+    const otherFilters = filters.find((filter) => filter.name === 'other')
+    const isMatureExcluded = otherFilters?.options?.some((option) => option.value === 'mature' && option.applied)
+    const isPromotionalExcluded = otherFilters?.options?.some(
+      (option) => option.value === 'promotional' && option.applied
+    )
+    const priceFilter = filters.find((filter) => filter.name === 'price')
+    const minPrice = priceFilter?.range?.appliedMin
+    const maxPrice = priceFilter?.range?.appliedMax
+
+    setHasAppliedFilters(
+      Boolean(minPrice || maxPrice || isPromotionalExcluded || isMatureExcluded || mappedStatus.length)
+    )
+
+    const commonFilters = {
+      lastSalePrice_gte: minPrice ? tokenNumberToHapiBn(minPrice).toString() : undefined,
+      lastSalePrice_lte: maxPrice ? tokenNumberToHapiBn(maxPrice).toString() : undefined,
+      video: {
+        ...(isMatureExcluded ? { isExcluded_eq: false } : {}),
+        ...(isPromotionalExcluded ? { hasMarketing_eq: false } : {}),
+      },
+    }
+    return {
+      OR: mappedStatus.length
+        ? mappedStatus.map((transactionalStatus) => ({
+            ...commonFilters,
+            transactionalStatus,
+          }))
+        : [commonFilters],
+    }
+  }, [filters])
+
+  const { nfts, loading, totalCount, fetchMore, pageInfo } = useNftsConnection({
+    where: mappedFilters,
+    orderBy: order,
+    first: limit,
+  })
+  const [isLoading, setIsLoading] = useState(false)
+
+  const placeholderItems = loading || isLoading ? createPlaceholderData(limit) : []
+  const nftsWithPlaceholders = [...(nfts || []), ...placeholderItems]
+  return (
+    <Section
+      headerProps={{
+        onApplyFilters: setFilters,
+        start: {
+          type: 'title',
+          title: 'All NFTs',
+          nodeEnd:
+            typeof totalCount === 'number' ? (
+              <NumberFormat value={totalCount} as="p" variant={smMatch ? 'h500' : 'h400'} color="colorTextMuted" />
+            ) : undefined,
+        },
+        filters,
+        sort: {
+          type: 'toggle-button',
+          toggleButtonOptionTypeProps: {
+            type: 'options',
+            options: ['Newest', 'Oldest'],
+            value: order === OwnedNftOrderByInput.CreatedAtDesc ? 'Newest' : 'Oldest',
+            onChange: (order) =>
+              setOrder(order === 'Oldest' ? OwnedNftOrderByInput.CreatedAtAsc : OwnedNftOrderByInput.CreatedAtDesc),
+          },
+        },
+      }}
+      contentProps={{
+        type: 'grid',
+        minChildrenWidth: 250,
+        children:
+          !(isLoading || loading) && !nfts?.length
+            ? [
+                <FallbackContainer key="fallback">
+                  <EmptyFallback
+                    title="No NFTs found"
+                    subtitle="Please, try changing your filtering criteria."
+                    button={
+                      hasAppliedFilters && (
+                        <Button variant="secondary" onClick={() => setFilters(FILTERS)}>
+                          Clear all filters
+                        </Button>
+                      )
+                    }
+                  />
+                </FallbackContainer>,
+              ]
+            : nftsWithPlaceholders.map((nft, idx) => <NftTileViewer key={idx} nftId={nft.id} />),
+      }}
+      footerProps={{
+        type: 'infinite',
+        reachedEnd: !pageInfo?.hasNextPage ?? true,
+        fetchMore: async () => {
+          setIsLoading(true)
+          await fetchMore({
+            variables: {
+              after: pageInfo?.endCursor,
+            },
+          }).finally(() => {
+            setIsLoading(false)
+          })
+        },
+      }}
+    />
+  )
+}
+
+export const FallbackContainer = styled.div`
+  grid-column: 1/-1;
+`

+ 1 - 0
packages/atlas/src/components/AllNftSection/index.ts

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

+ 12 - 3
packages/atlas/src/components/FilterButton/FilterButton.stories.tsx

@@ -3,7 +3,7 @@ import { useState } from 'react'
 
 import { SvgActionShoppingCart } from '@/assets/icons'
 
-import { FilterButton, FilterButtonOption, FilterButtonProps } from './FilterButton'
+import { FilterButton, FilterButtonOption, FilterButtonProps, isFilterRange } from './FilterButton'
 
 export default {
   title: 'inputs/FilterButton',
@@ -31,7 +31,14 @@ export default {
 const CheckboxTemplate: StoryFn<FilterButtonProps> = (args) => {
   const [options, setOptions] = useState<FilterButtonOption[]>(args.options || [])
 
-  return <FilterButton {...args} type="checkbox" options={options} onChange={setOptions} />
+  return (
+    <FilterButton
+      {...args}
+      type="checkbox"
+      options={options}
+      onChange={(val) => !isFilterRange(val) && setOptions(val)}
+    />
+  )
 }
 
 export const Checkbox = CheckboxTemplate.bind({})
@@ -39,7 +46,9 @@ export const Checkbox = CheckboxTemplate.bind({})
 const RadioTemplate: StoryFn<FilterButtonProps> = (args) => {
   const [options, setOptions] = useState<FilterButtonOption[]>(args.options || [])
 
-  return <FilterButton {...args} type="radio" options={options} onChange={setOptions} />
+  return (
+    <FilterButton {...args} type="radio" options={options} onChange={(val) => !isFilterRange(val) && setOptions(val)} />
+  )
 }
 
 export const Radio = RadioTemplate.bind({})

+ 1 - 1
packages/atlas/src/components/FilterButton/FilterButton.styles.ts

@@ -14,7 +14,7 @@ export const Counter = styled.div`
 `
 
 export const StyledButton = styled(Button)`
-  > svg > path {
+  svg > path {
     fill: ${cVar('colorTextMuted')};
   }
 `

+ 142 - 64
packages/atlas/src/components/FilterButton/FilterButton.tsx

@@ -1,7 +1,9 @@
-import { ChangeEvent, FC, ReactNode, useRef } from 'react'
+import { ChangeEvent, FC, ReactNode, useEffect, useRef, useState } from 'react'
+import { useInView } from 'react-intersection-observer'
 
 import { Counter, StyledButton } from '@/components/FilterButton/FilterButton.styles'
 import { CheckboxGroup } from '@/components/_inputs/CheckboxGroup'
+import { PriceRangeInput } from '@/components/_inputs/PriceRangeInput'
 import { DialogPopover } from '@/components/_overlays/DialogPopover'
 
 import { RadioButtonGroup } from '../_inputs/RadioButtonGroup'
@@ -13,85 +15,161 @@ export type FilterButtonOption = {
   applied: boolean
 }
 
+export type FilterRange = {
+  min?: number
+  max?: number
+  appliedMin?: number
+  appliedMax?: number
+}
+
+export const isFilterRange = (value: FilterRange | FilterButtonOption[]): value is FilterRange => 'min' in value
+
 export type FilterButtonProps = {
   name: string
-  type: 'checkbox' | 'radio'
   label?: string
   icon?: ReactNode
   className?: string
-  onChange: (selectedOptions: FilterButtonOption[]) => void
+  type: 'checkbox' | 'radio' | 'range'
+  onChange: (value: FilterRange | FilterButtonOption[]) => void
+  range?: FilterRange
   options?: FilterButtonOption[]
 }
 
 export type SectionFilter = Omit<FilterButtonProps, 'onChange'>
 
-export const FilterButton: FC<FilterButtonProps> = ({ type, name, onChange, className, icon, label, options = [] }) => {
-  const counter = options.filter((option) => option.applied)?.length
+export const FilterButton: FC<FilterButtonProps> = (props) => {
   const triggerRef = useRef<HTMLButtonElement>(null)
+  const firstRangeInput = useRef<HTMLInputElement | null>(null)
+  const [shouldFocus, setShouldFocus] = useState(false)
+  const { ref, inView } = useInView()
 
-  const handleApply = () => {
-    onChange(options.map((option) => ({ ...option, applied: option.selected })))
-    triggerRef.current?.click()
-  }
+  useEffect(() => {
+    if (inView && shouldFocus) {
+      firstRangeInput.current?.focus()
+      setShouldFocus(false)
+    }
+  }, [inView, shouldFocus])
 
-  const handleCheckboxSelection = (num: number) => {
-    const selected = options.map((option, idx) => {
-      if (num === idx) {
-        return { ...option, selected: !option.selected }
-      }
-      return option
-    })
-    onChange(selected)
-  }
+  if (props.type === 'checkbox' || props.type === 'radio') {
+    const { type, name, onChange, className, icon, label, options = [] } = props
 
-  const handleRadioButtonClick = (e: ChangeEvent<Omit<HTMLInputElement, 'value'> & { value: string | boolean }>) => {
-    const optionIdx = options.findIndex((option) => option.value === e.currentTarget.value)
-    const selected = options.map((option, idx) => ({ ...option, selected: optionIdx === idx }))
-    onChange(selected)
-  }
+    const counter = options.filter((option) => option.applied)?.length
+
+    const handleApply = () => {
+      onChange(options.map((option) => ({ ...option, applied: option.selected })))
+      triggerRef.current?.click()
+    }
+
+    const handleCheckboxSelection = (num: number) => {
+      const selected = options.map((option, idx) => {
+        if (num === idx) {
+          return { ...option, selected: !option.selected }
+        }
+        return option
+      })
+      onChange(selected)
+    }
+
+    const handleRadioButtonClick = (e: ChangeEvent<Omit<HTMLInputElement, 'value'> & { value: string | boolean }>) => {
+      const optionIdx = options.findIndex((option) => option.value === e.currentTarget.value)
+      const selected = options.map((option, idx) => ({ ...option, selected: optionIdx === idx }))
+      onChange(selected)
+    }
 
-  const handleClear = () => {
-    onChange(options.map((option) => ({ ...option, selected: false, applied: false })))
-    triggerRef.current?.click()
+    const handleClear = () => {
+      onChange(options.map((option) => ({ ...option, selected: false, applied: false })))
+      triggerRef.current?.click()
+    }
+
+    return (
+      <DialogPopover
+        className={className}
+        flipEnabled
+        appendTo={document.body}
+        trigger={
+          <StyledButton
+            ref={triggerRef}
+            icon={counter ? <Counter>{counter}</Counter> : icon}
+            iconPlacement="right"
+            variant="secondary"
+          >
+            {label}
+          </StyledButton>
+        }
+        primaryButton={{ text: 'Apply', onClick: handleApply }}
+        secondaryButton={{
+          text: 'Clear',
+          onClick: handleClear,
+        }}
+      >
+        {type === 'checkbox' && (
+          <CheckboxGroup
+            name={name}
+            options={options.map((option) => ({ ...option, value: option.selected }))}
+            checkedIds={options.map((option, index) => (option.selected ? index : -1)).filter((index) => index !== -1)}
+            onChange={handleCheckboxSelection}
+          />
+        )}
+        {type === 'radio' && (
+          <RadioButtonGroup
+            name={name}
+            options={options}
+            value={options.find((option) => option.selected)?.value}
+            onChange={handleRadioButtonClick}
+          />
+        )}
+      </DialogPopover>
+    )
   }
 
-  return (
-    <DialogPopover
-      className={className}
-      flipEnabled
-      appendTo={document.body}
-      trigger={
-        <StyledButton
-          ref={triggerRef}
-          icon={counter ? <Counter>{counter}</Counter> : icon}
-          iconPlacement="right"
-          variant="secondary"
-        >
-          {label}
-        </StyledButton>
-      }
-      primaryButton={{ text: 'Apply', onClick: handleApply }}
-      secondaryButton={{
-        text: 'Clear',
-        onClick: handleClear,
-      }}
-    >
-      {type === 'checkbox' && (
-        <CheckboxGroup
-          name={name}
-          options={options.map((option) => ({ ...option, value: option.selected }))}
-          checkedIds={options.map((option, index) => (option.selected ? index : -1)).filter((index) => index !== -1)}
-          onChange={handleCheckboxSelection}
-        />
-      )}
-      {type === 'radio' && (
-        <RadioButtonGroup
-          name={name}
-          options={options}
-          value={options.find((option) => option.selected)?.value}
-          onChange={handleRadioButtonClick}
+  if (props.type === 'range') {
+    const { onChange, className, icon, label, range } = props
+
+    const isApplied = Boolean(range?.appliedMax || range?.appliedMin)
+
+    const handleApply = () => {
+      onChange({ ...range, appliedMin: range?.min, appliedMax: range?.max })
+      triggerRef.current?.click()
+    }
+
+    const handleClear = () => {
+      onChange({ min: undefined, max: undefined, appliedMin: undefined, appliedMax: undefined })
+      triggerRef.current?.click()
+    }
+
+    return (
+      <DialogPopover
+        className={className}
+        flipEnabled
+        onShow={() => setShouldFocus(true)}
+        appendTo={document.body}
+        trigger={
+          <StyledButton
+            ref={triggerRef}
+            icon={isApplied ? <Counter>1</Counter> : icon}
+            iconPlacement="right"
+            variant="secondary"
+          >
+            {label}
+          </StyledButton>
+        }
+        primaryButton={{ text: 'Apply', onClick: handleApply }}
+        secondaryButton={{
+          text: 'Clear',
+          onClick: handleClear,
+        }}
+      >
+        <PriceRangeInput
+          ref={(inputRef) => {
+            ref(inputRef)
+            firstRangeInput.current = inputRef
+          }}
+          value={range}
+          onChange={(value) => onChange({ ...range, ...value })}
         />
-      )}
-    </DialogPopover>
-  )
+      </DialogPopover>
+    )
+  }
+
+  return null
 }

+ 21 - 1
packages/atlas/src/components/MobileFilterButton/MobileFilterButton.tsx

@@ -8,6 +8,7 @@ import { MobileFilterContainer } from '../FiltersBar/FiltersBar.styles'
 import { Text } from '../Text'
 import { Button } from '../_buttons/Button'
 import { CheckboxGroup } from '../_inputs/CheckboxGroup'
+import { InputRange, PriceRangeInput } from '../_inputs/PriceRangeInput'
 import { RadioButtonGroup } from '../_inputs/RadioButtonGroup'
 import { DialogModal, DialogModalProps } from '../_overlays/DialogModal'
 
@@ -26,6 +27,7 @@ export const MobileFilterButton: FC<MobileFilterButtonProps> = ({ filters, onCha
     const newFilters = filters.map((filter) => ({
       ...filter,
       options: filter.options?.map((option) => ({ ...option, applied: option.selected })),
+      range: { ...filter.range, appliedMin: filter.range?.min, appliedMax: filter.range?.max },
     }))
 
     onChangeFilters?.(newFilters)
@@ -80,10 +82,25 @@ export const MobileFilterButton: FC<MobileFilterButtonProps> = ({ filters, onCha
     onChangeFilters?.(newFilters)
   }
 
+  const handleRangeChange = (name: string, range: InputRange) => {
+    const newFilters = filters.map((filter) => {
+      if (filter.name === name) {
+        return {
+          ...filter,
+          range,
+        }
+      }
+      return filter
+    })
+
+    onChangeFilters?.(newFilters)
+  }
+
   const handleClear = () => {
     const newFilters = filters.map((filter) => ({
       ...filter,
       options: filter.options?.map((option) => ({ ...option, selected: false, applied: false })),
+      range: { min: undefined, max: undefined, appliedMin: undefined, appliedMax: undefined },
     }))
 
     onChangeFilters?.(newFilters)
@@ -114,7 +131,7 @@ export const MobileFilterButton: FC<MobileFilterButtonProps> = ({ filters, onCha
         show={isFiltersOpen}
         content={
           <>
-            {filters.map(({ name, options = [], label, type }, idx) => (
+            {filters.map(({ name, options = [], label, type, range }, idx) => (
               <MobileFilterContainer key={idx}>
                 <Text as="span" variant="h300">
                   {label}
@@ -137,6 +154,9 @@ export const MobileFilterButton: FC<MobileFilterButtonProps> = ({ filters, onCha
                     value={options.find((option) => option.selected)?.value}
                   />
                 )}
+                {type === 'range' && (
+                  <PriceRangeInput value={range} onChange={(newRange) => handleRangeChange(name, newRange)} />
+                )}
               </MobileFilterContainer>
             ))}
           </>

+ 1 - 0
packages/atlas/src/components/Section/Section.stories.tsx

@@ -237,6 +237,7 @@ const DefaultTemplate: StoryFn<SectionProps> = () => {
         footerProps={{
           type: 'infinite',
           fetchMore: async () => setSecondPlaceholdersCount((count) => count + 8),
+          reachedEnd: secondPlaceholderItems.length > 40,
         }}
       />
     </div>

+ 1 - 1
packages/atlas/src/components/Section/SectionContent/SectionContent.styles.ts

@@ -4,7 +4,7 @@ import { media, sizes } from '@/styles'
 
 export const GridWrapper = styled.div<{ minWidth: number }>`
   display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(${(props) => `${props.minWidth}px`}, 1fr));
+  grid-template-columns: repeat(auto-fill, minmax(${(props) => `${props.minWidth}px, 1fr`}));
   gap: ${sizes(4)};
 
   ${media.md} {

+ 1 - 0
packages/atlas/src/components/Section/SectionFooter/SectionFooter.stories.tsx

@@ -88,6 +88,7 @@ const InfiniteTemplate: StoryFn<{ type: 'infinite' | 'load' }> = ({ type }) => {
       </Grid>
       <SectionFooter
         type={type}
+        reachedEnd={items > 50}
         label="Load more boxes"
         fetchMore={() =>
           new Promise((res) => {

+ 8 - 1
packages/atlas/src/components/Section/SectionFooter/SectionFooter.tsx

@@ -16,6 +16,7 @@ type SectionFooterLoadProps = {
   type: 'load'
   label: string
   fetchMore: () => Promise<void>
+  reachedEnd: boolean
 }
 
 type SectionFooterPaginationProps = {
@@ -25,6 +26,7 @@ type SectionFooterPaginationProps = {
 type SectionFooterInfiniteLoadingProps = {
   type: 'infinite'
   fetchMore: () => Promise<void>
+  reachedEnd: boolean
 }
 
 export type SectionFooterProps =
@@ -43,7 +45,12 @@ export const SectionFooter = (props: SectionFooterProps) => {
   const { ref, inView } = useInView()
 
   useEffect(() => {
-    if ((props.type === 'infinite' || (props.type === 'load' && isSwitchedToInfinite)) && inView && !isLoading) {
+    if (
+      (props.type === 'infinite' || (props.type === 'load' && isSwitchedToInfinite)) &&
+      !props.reachedEnd &&
+      inView &&
+      !isLoading
+    ) {
       setIsLoading(true)
       props.fetchMore().finally(() => setIsLoading(false))
     }

+ 9 - 4
packages/atlas/src/components/Section/SectionHeader/SectionFilters/SectionFilters.tsx

@@ -1,7 +1,7 @@
 import { FC, useRef } from 'react'
 
 import { SvgActionChevronL, SvgActionChevronR, SvgActionClose } from '@/assets/icons'
-import { FilterButton, FilterButtonOption, SectionFilter } from '@/components/FilterButton'
+import { FilterButton, FilterButtonOption, FilterRange, SectionFilter, isFilterRange } from '@/components/FilterButton'
 import { MobileFilterButton } from '@/components/MobileFilterButton'
 import { Button } from '@/components/_buttons/Button'
 import { useHorizonthalFade } from '@/hooks/useHorizonthalFade'
@@ -28,15 +28,19 @@ export const SectionFilters: FC<SectionFiltersProps> = ({ filters, onApplyFilter
   const { handleMouseDown, visibleShadows, handleArrowScroll, isOverflow } = useHorizonthalFade(filterWrapperRef)
 
   const areThereAnyOptionsSelected = filters
-    .map((filter) => filter.options?.map((option) => option.applied))
+    .map(
+      (filter) =>
+        filter.options?.map((option) => option.applied) ?? (filter.range?.appliedMin || filter.range?.appliedMax)
+    )
     .flat()
     .some(Boolean)
 
-  const handleApply = (name: string, selectedOptions: FilterButtonOption[]) => {
+  const handleApply = (name: string, selectedOptions: FilterButtonOption[] | FilterRange) => {
     onApplyFilters?.(
       filters.map((filter) => {
         if (filter.name === name) {
-          return { ...filter, options: selectedOptions }
+          const isFilter = isFilterRange(selectedOptions)
+          return { ...filter, [isFilter ? 'range' : 'options']: selectedOptions }
         }
         return filter
       })
@@ -47,6 +51,7 @@ export const SectionFilters: FC<SectionFiltersProps> = ({ filters, onApplyFilter
     const newFilters = filters.map((filter) => ({
       ...filter,
       options: filter.options?.map((option) => ({ ...option, selected: false, applied: false })),
+      range: { min: undefined, max: undefined, maxApplied: undefined, minApplied: undefined },
     }))
 
     onApplyFilters?.(newFilters)

+ 4 - 2
packages/atlas/src/components/Section/SectionHeader/SectionHeader.tsx

@@ -55,6 +55,7 @@ type SectionHeaderStart =
       type: 'title'
       title: string
       nodeStart?: TitleNodeStart
+      nodeEnd?: ReactNode
     }
   | {
       type: 'tabs'
@@ -94,7 +95,7 @@ export const SectionHeader: FC<SectionHeaderProps> = (props) => {
         <MobileFirstRow>
           {!isSearchInputOpen && (
             <>
-              {start.type === 'title' && <SectionTitleComponent nodeStart={start.nodeStart} title={start.title} />}
+              {start.type === 'title' && <SectionTitleComponent {...start} />}
               {start.type === 'tabs' && <Tabs {...start.tabsProps} />}
             </>
           )}
@@ -125,6 +126,7 @@ export const SectionHeader: FC<SectionHeaderProps> = (props) => {
         <MobileSecondRow>
           {filters && !filtersInFirstRow && <SectionFilters filters={filters} onApplyFilters={onApplyFilters} />}
           {sort?.type === 'select' && <Select {...sort.selectProps} size="medium" />}
+          {sort?.type === 'toggle-button' && <ToggleButtonGroup {...sort.toggleButtonOptionTypeProps} width="fluid" />}
         </MobileSecondRow>
       </SectionHeaderWrapper>
     )
@@ -135,7 +137,7 @@ export const SectionHeader: FC<SectionHeaderProps> = (props) => {
   return (
     <SectionHeaderWrapper isTabs={start.type === 'tabs'}>
       <StartWrapper enableHorizonthalScrolling={start.type === 'tabs'}>
-        {start.type === 'title' && <SectionTitleComponent nodeStart={start.nodeStart} title={start.title} />}
+        {start.type === 'title' && <SectionTitleComponent {...start} />}
         {start.type === 'tabs' && <Tabs {...start.tabsProps} />}
       </StartWrapper>
       {search && <DynamicSearch search={search} isOpen={isSearchInputOpen} onSearchToggle={setIsSearchInputOpen} />}

+ 3 - 1
packages/atlas/src/components/Section/SectionHeader/SectionTitle/SectionTitle.tsx

@@ -22,10 +22,11 @@ type TitleNodeStart =
 
 type SectionTitleComponentProps = {
   nodeStart?: TitleNodeStart
+  nodeEnd?: ReactNode
   title: string
 }
 
-export const SectionTitleComponent: FC<SectionTitleComponentProps> = ({ nodeStart, title }) => {
+export const SectionTitleComponent: FC<SectionTitleComponentProps> = ({ nodeStart, title, nodeEnd }) => {
   const smMatch = useMediaMatch('sm')
 
   const renderNodeStart = () => {
@@ -46,6 +47,7 @@ export const SectionTitleComponent: FC<SectionTitleComponentProps> = ({ nodeStar
       <HeaderTitle variant={smMatch ? 'h500' : 'h400'} as="h3">
         {title}
       </HeaderTitle>
+      {nodeEnd}
     </HeaderTitleWrapper>
   )
 }

+ 1 - 1
packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.stories.tsx

@@ -11,7 +11,7 @@ export default {
 
 const Template: StoryFn<CallToActionButtonProps> = (args) => {
   return (
-    <CallToActionWrapper>
+    <CallToActionWrapper itemsCount={1}>
       <CallToActionButton {...args} />
     </CallToActionWrapper>
   )

+ 11 - 5
packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.styles.ts

@@ -15,13 +15,19 @@ const mappedColors = {
   white: cVar('colorCoreNeutral50'),
 }
 
-export const CallToActionWrapper = styled.div`
-  margin-top: ${sizes(32)};
+export const CallToActionWrapper = styled.div<{ itemsCount: number }>`
+  display: grid;
+  gap: ${sizes(4)};
+  padding-bottom: ${sizes(16)};
+  justify-items: center;
 
   ${media.md} {
-    display: grid;
-    grid-template-columns: 1fr 1fr 1fr;
-    grid-column-gap: ${sizes(6)};
+    margin: 0 auto;
+    justify-content: center;
+    grid-template-columns: repeat(auto-fit, minmax(219px, 1fr));
+    max-width: ${({ itemsCount }) => `calc(${itemsCount - 1} * ${sizes(6)} + 419px * ${itemsCount}) `};
+    gap: ${sizes(6)};
+    padding-bottom: ${sizes(24)};
   }
 `
 type IconWrapperProps = {

+ 0 - 15
packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.tsx

@@ -4,10 +4,8 @@ import { FC, MouseEvent, ReactNode } from 'react'
 import {
   SvgActionChevronR,
   SvgActionNewTab,
-  SvgSidebarChannels,
   SvgSidebarExplore,
   SvgSidebarHome,
-  SvgSidebarNew,
   SvgSidebarPopular,
 } from '@/assets/icons'
 import { atlasConfig } from '@/config'
@@ -58,19 +56,6 @@ export const CTA_MAP: Record<string, CallToActionButtonProps> = {
     colorVariant: 'yellow',
     icon: <SvgSidebarHome />,
   },
-  new: {
-    label: 'New & Noteworthy',
-    to: absoluteRoutes.viewer.new(),
-    colorVariant: 'green',
-    icon: <SvgSidebarNew />,
-  },
-  channels: {
-    label: 'Browse channels',
-    to: absoluteRoutes.viewer.channels(),
-    colorVariant: 'blue',
-    iconColorVariant: 'lightBlue',
-    icon: <SvgSidebarChannels />,
-  },
   popular: {
     label: `Popular on ${atlasConfig.general.appName}`,
     to: absoluteRoutes.viewer.popular(),

+ 61 - 0
packages/atlas/src/components/_buttons/GoogleButton/GoogleButton.tsx

@@ -0,0 +1,61 @@
+import styled from '@emotion/styled'
+import * as React from 'react'
+
+import { SvgGoogleSmallLogo } from '@/assets/icons/GoogleSmallLogo'
+
+interface GoogleButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
+  label?: string
+  disabled?: boolean
+}
+export const GoogleButton = ({ label, ...rest }: GoogleButtonProps) => {
+  return (
+    <StyledButton {...rest}>
+      <IconContainer>
+        <SvgGoogleSmallLogo />
+      </IconContainer>
+      <span>Sign in with Google</span>
+    </StyledButton>
+  )
+}
+
+const IconContainer = styled.div`
+  background-color: #fff;
+  height: 100%;
+  display: grid;
+  place-items: center;
+`
+
+const StyledButton = styled.button`
+  display: inline-flex;
+  align-items: center;
+  text-align: center;
+  padding: 0;
+  box-shadow: 0 2px 4px 0 rgb(0 0 0 / 0.25);
+  font-size: 16px;
+  border-radius: 1px;
+  transition: background-color 0.218s, border-color 0.218s, box-shadow 0.218s;
+  font-family: Roboto, arial, sans-serif;
+  cursor: pointer;
+  user-select: none;
+  background-color: #4285f4;
+  color: #fff;
+  border: 1px solid #4285f4;
+
+  :hover {
+    box-shadow: 0 0 3px 3px rgb(66 133 244 / 0.3);
+  }
+
+  :disabled {
+    background-color: rgb(37 5 5 / 0.08);
+    color: rgb(0 0 0 / 0.4);
+    cursor: not-allowed;
+  }
+
+  > span {
+    padding: 0 12px;
+    width: 100%;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+`

+ 1 - 0
packages/atlas/src/components/_buttons/GoogleButton/index.ts

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

+ 17 - 7
packages/atlas/src/components/_channel/ChannelCard/ChannelCard.stories.tsx

@@ -1,5 +1,6 @@
 import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'
 import { Meta, StoryFn } from '@storybook/react'
+import { QueryClient, QueryClientProvider } from 'react-query'
 import { BrowserRouter } from 'react-router-dom'
 
 // import { createApolloClient } from '@/api'
@@ -29,16 +30,25 @@ export default {
   decorators: [
     (Story) => {
       const apolloClient = new ApolloClient({ cache: new InMemoryCache() })
+      const queryClient = new QueryClient({
+        defaultOptions: {
+          queries: {
+            refetchOnWindowFocus: false,
+          },
+        },
+      })
       return (
         <BrowserRouter>
           <ApolloProvider client={apolloClient}>
-            <OverlayManagerProvider>
-              <OperatorsContextProvider>
-                <ConfirmationModalProvider>
-                  <Story />
-                </ConfirmationModalProvider>
-              </OperatorsContextProvider>
-            </OverlayManagerProvider>
+            <QueryClientProvider client={queryClient}>
+              <OverlayManagerProvider>
+                <OperatorsContextProvider>
+                  <ConfirmationModalProvider>
+                    <Story />
+                  </ConfirmationModalProvider>
+                </OperatorsContextProvider>
+              </OverlayManagerProvider>
+            </QueryClientProvider>
           </ApolloProvider>
         </BrowserRouter>
       )

+ 45 - 0
packages/atlas/src/components/_channel/ChannelsSection/ChannelsSection.stories.tsx

@@ -0,0 +1,45 @@
+import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'
+import { Meta, StoryFn } from '@storybook/react'
+import { QueryClient, QueryClientProvider } from 'react-query'
+import { BrowserRouter } from 'react-router-dom'
+
+import { ChannelsSection } from '@/components/_channel/ChannelsSection/ChannelsSection'
+import { OperatorsContextProvider } from '@/providers/assets/assets.provider'
+import { ConfirmationModalProvider } from '@/providers/confirmationModal'
+import { OverlayManagerProvider } from '@/providers/overlayManager'
+
+export default {
+  title: 'Channel/ChannelsSection',
+  component: ChannelsSection,
+  decorators: [
+    (Story) => {
+      const apolloClient = new ApolloClient({ cache: new InMemoryCache() })
+      const queryClient = new QueryClient({
+        defaultOptions: {
+          queries: {
+            refetchOnWindowFocus: false,
+          },
+        },
+      })
+      return (
+        <BrowserRouter>
+          <ApolloProvider client={apolloClient}>
+            <QueryClientProvider client={queryClient}>
+              <OverlayManagerProvider>
+                <OperatorsContextProvider>
+                  <ConfirmationModalProvider>
+                    <Story />
+                  </ConfirmationModalProvider>
+                </OperatorsContextProvider>
+              </OverlayManagerProvider>
+            </QueryClientProvider>
+          </ApolloProvider>
+        </BrowserRouter>
+      )
+    },
+  ],
+} as Meta
+
+export const Default: StoryFn = () => {
+  return <ChannelsSection />
+}

+ 69 - 0
packages/atlas/src/components/_channel/ChannelsSection/ChannelsSection.tsx

@@ -0,0 +1,69 @@
+import { useState } from 'react'
+
+import { useBasicChannelsConnection } from '@/api/hooks/channelsConnection'
+import { ChannelOrderByInput } from '@/api/queries/__generated__/baseTypes.generated'
+import { Section } from '@/components/Section/Section'
+import { ChannelCard } from '@/components/_channel/ChannelCard'
+
+export const ChannelsSection = () => {
+  const [sortBy, setSortBy] = useState<string>('Most followed')
+  const {
+    edges: channels,
+    pageInfo,
+    loading,
+    fetchMore,
+  } = useBasicChannelsConnection({
+    orderBy:
+      sortBy === 'Newest'
+        ? ChannelOrderByInput.CreatedAtDesc
+        : sortBy === 'Oldest'
+        ? ChannelOrderByInput.CreatedAtAsc
+        : ChannelOrderByInput.FollowsNumDesc,
+    first: 10,
+  })
+  const [isLoading, setIsLoading] = useState(false)
+
+  if (!channels || (!channels?.length && !(loading || isLoading))) {
+    return null
+  }
+
+  return (
+    <Section
+      headerProps={{
+        start: {
+          title: 'Channels',
+          type: 'title',
+        },
+        sort: {
+          type: 'toggle-button',
+          toggleButtonOptionTypeProps: {
+            type: 'options',
+            options: ['Newest', 'Oldest', 'Most followed'],
+            value: sortBy,
+            onChange: setSortBy,
+          },
+        },
+      }}
+      contentProps={{
+        type: 'grid',
+        minChildrenWidth: 200,
+        children: channels.map(({ node }) => <ChannelCard key={node.id} channel={node} />),
+      }}
+      footerProps={{
+        type: 'load',
+        label: 'Load more channels',
+        reachedEnd: !pageInfo?.hasNextPage ?? true,
+        fetchMore: async () => {
+          setIsLoading(true)
+          await fetchMore({
+            variables: {
+              after: pageInfo?.endCursor,
+            },
+          }).finally(() => {
+            setIsLoading(false)
+          })
+        },
+      }}
+    />
+  )
+}

+ 1 - 0
packages/atlas/src/components/_channel/ChannelsSection/index.ts

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

+ 2 - 16
packages/atlas/src/components/_channel/ExpandableChannelsList/ExpandableChannelsList.tsx

@@ -1,7 +1,7 @@
 import { QueryHookOptions } from '@apollo/client'
 import { FC, Fragment, useState } from 'react'
 
-import { useBasicChannels, useDiscoverChannels, usePopularChannels, usePromisingChannels } from '@/api/hooks/channel'
+import { useBasicChannels, useDiscoverChannels, usePopularChannels } from '@/api/hooks/channel'
 import { ChannelOrderByInput } from '@/api/queries/__generated__/baseTypes.generated'
 import { SvgActionChevronR } from '@/assets/icons'
 import { EmptyFallback } from '@/components/EmptyFallback'
@@ -22,7 +22,7 @@ import {
   Separator,
 } from './ExpandableChannelsList.styles'
 
-type ChannelsQueryType = 'discover' | 'promising' | 'popular' | 'regular'
+type ChannelsQueryType = 'discover' | 'popular' | 'regular'
 
 type ExpandableChannelsListProps = {
   queryType: ChannelsQueryType
@@ -166,18 +166,6 @@ const useChannelsListData = (queryType: ChannelsQueryType, selectedLanguage: str
     { ...commonOpts, skip: queryType !== 'popular' }
   )
 
-  const promising = usePromisingChannels(
-    {
-      where: {
-        activeVideosCount_gt: activeVideosCountGt,
-        channel: {
-          ...publicChannelFilter,
-          videoViewsNum_gt: 0,
-        },
-      },
-    },
-    { ...commonOpts, skip: queryType !== 'promising' }
-  )
   // regular channels query needs explicit limit and sorting as it's not defined by Orion
   const regular = useBasicChannels(
     {
@@ -198,8 +186,6 @@ const useChannelsListData = (queryType: ChannelsQueryType, selectedLanguage: str
     return discover
   } else if (queryType === 'popular') {
     return popular
-  } else if (queryType === 'promising') {
-    return promising
   } else {
     return regular
   }

+ 48 - 0
packages/atlas/src/components/_inputs/PriceRangeInput/PriceRangeInput.tsx

@@ -0,0 +1,48 @@
+import styled from '@emotion/styled'
+import { forwardRef } from 'react'
+
+import { SvgJoyTokenMonochrome16 } from '@/assets/icons'
+import { Input } from '@/components/_inputs/Input'
+import { sizes } from '@/styles'
+
+export type InputRange = {
+  min?: number
+  max?: number
+}
+
+type RangeInputsProps = {
+  value?: InputRange
+  onChange?: (value: InputRange) => void
+}
+export const PriceRangeInput = forwardRef<HTMLInputElement, RangeInputsProps>(
+  ({ onChange, value }: RangeInputsProps, ref) => {
+    return (
+      <InputsContainer>
+        <Input
+          ref={ref}
+          type="number"
+          size="medium"
+          nodeStart={<SvgJoyTokenMonochrome16 />}
+          placeholder="Min"
+          value={value?.min ?? ''}
+          onChange={(e) => onChange?.({ ...value, min: parseInt(e.target.value, 10) })}
+        />
+        <Input
+          type="number"
+          size="medium"
+          nodeStart={<SvgJoyTokenMonochrome16 />}
+          placeholder="Max"
+          value={value?.max ?? ''}
+          onChange={(e) => onChange?.({ ...value, max: parseInt(e.target.value, 10) })}
+        />
+      </InputsContainer>
+    )
+  }
+)
+
+PriceRangeInput.displayName = 'PriceRangeInput'
+
+const InputsContainer = styled.div`
+  display: flex;
+  gap: ${sizes(2)};
+`

+ 1 - 0
packages/atlas/src/components/_inputs/PriceRangeInput/index.ts

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

+ 7 - 0
packages/atlas/src/components/_inputs/ToggleButtonGroup/ToggleButtonGroup.styles.ts

@@ -25,6 +25,7 @@ export const Container = styled.div<{ width: ContainerWidth }>`
   box-shadow: inset 0 0 0 1px ${cVar('colorBorderMutedAlpha')};
   border-radius: ${cVar('radiusSmall')};
   max-width: ${(props) => getContainerMaxWidth(props.width)};
+  width: 100%;
 `
 
 export const OptionWrapper = styled.div<MaskProps>`
@@ -42,6 +43,12 @@ export const OptionWrapper = styled.div<MaskProps>`
   }
 `
 
+export const ToggleButton = styled(Button)`
+  span {
+    white-space: nowrap;
+  }
+`
+
 export const Label = styled(Text)`
   padding: ${sizes(2)};
   align-self: center;

+ 3 - 3
packages/atlas/src/components/_inputs/ToggleButtonGroup/ToggleButtonGroup.tsx

@@ -2,7 +2,6 @@ import { useRef } from 'react'
 
 import { SvgActionChevronL, SvgActionChevronR } from '@/assets/icons'
 import { FilterButton, FilterButtonProps } from '@/components/FilterButton'
-import { Button } from '@/components/_buttons/Button'
 import { useHorizonthalFade } from '@/hooks/useHorizonthalFade'
 
 import {
@@ -13,6 +12,7 @@ import {
   ContentWrapper,
   Label,
   OptionWrapper,
+  ToggleButton,
 } from './ToggleButtonGroup.styles'
 
 type SharedToggleButtonProps = {
@@ -63,7 +63,7 @@ export const ToggleButtonGroup = <T extends string = string>(props: ToggleButton
         <OptionWrapper onMouseDown={handleMouseDown} ref={optionWrapperRef} visibleShadows={visibleShadows}>
           {type === 'options' &&
             props.options.map((option) => (
-              <Button
+              <ToggleButton
                 key={option}
                 fullWidth
                 variant={option !== props.value ? 'tertiary' : 'secondary'}
@@ -71,7 +71,7 @@ export const ToggleButtonGroup = <T extends string = string>(props: ToggleButton
                 size="small"
               >
                 {option}
-              </Button>
+              </ToggleButton>
             ))}
           {type === 'filter' &&
             props.filters.map((filterButtonProps, idx) => <FilterButton key={idx} {...filterButtonProps} />)}

+ 0 - 15
packages/atlas/src/components/_navigation/SidenavViewer/SidenavViewer.tsx

@@ -3,11 +3,9 @@ import { FC, useState } from 'react'
 import {
   SvgActionMember,
   SvgActionNewTab,
-  SvgSidebarChannels,
   SvgSidebarExplore,
   SvgSidebarHome,
   SvgSidebarMarketplace,
-  SvgSidebarNew,
   SvgSidebarPopular,
   SvgSidebarYpp,
 } from '@/assets/icons'
@@ -44,25 +42,12 @@ export const viewerNavItems = [
     to: absoluteRoutes.viewer.nfts(),
     bottomNav: true,
   },
-  {
-    icon: <SvgSidebarNew />,
-    expandedName: 'New & Noteworthy',
-    name: 'New',
-    to: absoluteRoutes.viewer.new(),
-    bottomNav: true,
-  },
   {
     icon: <SvgSidebarExplore />,
     name: 'Discover',
     to: absoluteRoutes.viewer.discover(),
     bottomNav: false,
   },
-  {
-    icon: <SvgSidebarChannels />,
-    name: 'Channels',
-    to: absoluteRoutes.viewer.channels(),
-    bottomNav: true,
-  },
   ...(atlasConfig.features.ypp.googleConsoleClientId
     ? [
         {

+ 9 - 6
packages/atlas/src/components/_overlays/Dialog/Dialog.tsx

@@ -26,7 +26,7 @@ export type DialogProps = PropsWithChildren<{
   title?: ReactNode
   dividers?: boolean
   size?: DialogSize
-  primaryButton?: DialogButtonProps
+  primaryButton?: DialogButtonProps | JSX.Element
   secondaryButton?: DialogButtonProps
   additionalActionsNode?: ReactNode
   additionalActionsNodeMobilePosition?: 'top' | 'bottom'
@@ -111,11 +111,14 @@ export const Dialog: FC<DialogProps> = ({
                 {secondaryButton.text}
               </Button>
             )}
-            {primaryButton && (
-              <Button variant={primaryButton.variant || 'primary'} size={buttonSize} fullWidth {...primaryButton}>
-                {primaryButton.text}
-              </Button>
-            )}
+            {primaryButton &&
+              ('text' in primaryButton ? (
+                <Button variant={primaryButton.variant || 'primary'} size={buttonSize} fullWidth {...primaryButton}>
+                  {primaryButton.text}
+                </Button>
+              ) : (
+                (primaryButton as JSX.Element)
+              ))}
           </FooterButtonsContainer>
         </Footer>
       )}

+ 1 - 1
packages/atlas/src/components/_overlays/DialogModal/DialogModal.stories.tsx

@@ -57,7 +57,7 @@ const ToggleableTemplate: StoryFn<DialogModalProps> = ({ ...args }) => {
         onExitClick={() => setOpen(false)}
         primaryButton={{
           ...args.primaryButton,
-          text: args.primaryButton?.text || 'Default',
+          text: args.primaryButton && 'text' in args.primaryButton ? args.primaryButton?.text : 'Default',
           onClick: () => setOpen(false),
         }}
         secondaryButton={{

+ 1 - 1
packages/atlas/src/components/_templates/VideoContentTemplate.tsx

@@ -28,7 +28,7 @@ export const VideoContentTemplate: FC<VideoContentTemplateProps> = ({ children,
         </Text>
       )}
       {children}
-      {cta && <CallToActionWrapper>{ctaContent}</CallToActionWrapper>}
+      {cta && <CallToActionWrapper itemsCount={cta.length}>{ctaContent}</CallToActionWrapper>}
     </StyledViewWrapper>
   )
 }

+ 0 - 1
packages/atlas/src/config/routes.ts

@@ -22,7 +22,6 @@ export const relativeRoutes = {
   },
   viewer: {
     index: () => '',
-    new: () => 'new',
     discover: () => 'discover',
     popular: () => 'popular',
     category: (id = ':id') => `category/${id}`,

+ 1 - 1
packages/atlas/src/index.html

@@ -17,7 +17,7 @@
     <link rel="preconnect" href="https://fonts.googleapis.com" />
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
     <link
-      href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@600;700&family=Inter:wght@400;500;600;700&display=swap"
+      href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@600;700&family=Roboto:wght@400&family=Inter:wght@400;500;600;700&display=swap"
       rel="stylesheet"
     />
   </head>

+ 2 - 2
packages/atlas/src/providers/joystream/joystream.hooks.ts

@@ -172,14 +172,14 @@ export const useSubscribeAccountBalance = (
   )
 
   useEffect(() => {
-    if (!activeMembership?.controllerAccount || !joystream) {
+    if (!(controllerAccount || activeMembership?.controllerAccount) || !joystream) {
       return
     }
 
     let unsubscribe: (() => void) | undefined
     const init = async () => {
       unsubscribe = await joystream.subscribeAccountBalance(
-        controllerAccount || activeMembership.controllerAccount,
+        (controllerAccount || activeMembership?.controllerAccount) ?? '',
         proxyCallback(({ availableBalance, lockedBalance, totalInvitationLock }) => {
           setLockedAccountBalance(new BN(lockedBalance))
           setTotalInvitationLock(new BN(totalInvitationLock))

+ 5 - 4
packages/atlas/src/providers/transactions/transactions.hooks.ts

@@ -51,11 +51,12 @@ type HandleTransactionOpts<T extends ExtrinsicResult> = {
 }
 type HandleTransactionFn = <T extends ExtrinsicResult>(opts: HandleTransactionOpts<T>) => Promise<boolean>
 
+const WALLETS_WITH_METADATA = ['talisman', 'polkadot-js', 'subwallet-js']
+
 export const useTransaction = (): HandleTransactionFn => {
   const { addBlockAction, addTransaction, updateTransaction, removeTransaction } = useTransactionManagerStore(
     (state) => state.actions
   )
-  const userWalletName = useUserStore((state) => state.wallet?.title)
   const navigate = useNavigate()
 
   const [openOngoingTransactionModal, closeOngoingTransactionModal] = useConfirmationModal()
@@ -87,7 +88,7 @@ export const useTransaction = (): HandleTransactionFn => {
         return false
       }
 
-      if (isSignerMetadataOutdated) {
+      if (isSignerMetadataOutdated && WALLETS_WITH_METADATA.includes(wallet?.extensionName ?? '')) {
         await new Promise((resolve) => {
           openOngoingTransactionModal({
             title: 'Update Wallet Metadata',
@@ -136,7 +137,7 @@ export const useTransaction = (): HandleTransactionFn => {
           title: anyUnsignedTransaction ? 'Sign outstanding transactions' : 'Wait for other transactions',
           type: 'informative',
           description: anyUnsignedTransaction
-            ? `You have outstanding blockchain transactions waiting for you to sign them in ${userWalletName}. Please, sign or cancel previous transactions in ${userWalletName} to continue.`
+            ? `You have outstanding blockchain transactions waiting for you to sign them in ${wallet?.title}. Please, sign or cancel previous transactions in ${wallet?.title} to continue.`
             : 'You have other blockchain transactions which are still being processed. Please, try again in about a minute.',
           primaryButton: {
             text: 'Got it',
@@ -344,7 +345,7 @@ export const useTransaction = (): HandleTransactionFn => {
       totalBalance,
       updateSignerMetadata,
       updateTransaction,
-      userWalletName,
+      wallet?.extensionName,
       wallet?.title,
     ]
   )

+ 1 - 1
packages/atlas/src/providers/uploads/uploads.hooks.ts

@@ -32,7 +32,7 @@ export const useStartFileUpload = () => {
   const navigate = useNavigate()
   const { displaySnackbar } = useSnackbar()
   const { getClosestStorageOperatorForBag, markStorageOperatorFailed } = useStorageOperators()
-  const { mutateAsync: uploadMutation } = useMutation('subtitles-fetch', (params: MutationParams) =>
+  const { mutateAsync: uploadMutation } = useMutation('upload-assets', (params: MutationParams) =>
     axios.post(params.url, params.data, params.config)
   )
 

+ 1 - 1
packages/atlas/src/types/cta.ts

@@ -1 +1 @@
-export type CtaData = 'home' | 'new' | 'channels' | 'popular'
+export type CtaData = 'home' | 'channels' | 'popular'

+ 27 - 0
packages/atlas/src/utils/asset.test.ts

@@ -0,0 +1,27 @@
+import { createAssetUploadEndpoint } from './asset'
+
+const urlWithNoSlash = 'https://example.com/colossus-2'
+const urlWithSlash = 'https://example.com/colossus-2/'
+const notUrl = 'notaurl'
+const mockedUploadParams = {
+  'dataObjectId': '9999',
+  'storageBucketId': '999',
+  'bagId': 'dynamic:channel:999',
+}
+
+const expectedUrl =
+  'https://example.com/colossus-2/api/v1/files?dataObjectId=9999&storageBucketId=999&bagId=dynamic%3Achannel%3A999'
+
+describe('createAssetUploadEndpoint', () => {
+  it('should return correct url when there is no slash in the end', () => {
+    expect(createAssetUploadEndpoint(urlWithNoSlash, mockedUploadParams)).toEqual(expectedUrl)
+  })
+
+  it('should return correct url when there is slash in the end', () => {
+    expect(createAssetUploadEndpoint(urlWithSlash, mockedUploadParams)).toEqual(expectedUrl)
+  })
+
+  it('should throw error when the url is incorrect', () => {
+    expect(() => createAssetUploadEndpoint(notUrl, mockedUploadParams)).toThrowError()
+  })
+})

+ 10 - 9
packages/atlas/src/utils/asset.ts

@@ -12,15 +12,16 @@ type UploadRequestParams = {
   bagId: string
 }
 export const createAssetUploadEndpoint = (operatorEndpoint: string, uploadParams: UploadRequestParams) => {
-  const uploadEndpoint = new URL(atlasConfig.storage.uploadPath, operatorEndpoint)
-  Object.entries(uploadParams).forEach(([key, value]) => {
-    uploadEndpoint.searchParams.set(key, value)
-  })
-  return uploadEndpoint.toString()
-}
-
-export const createAssetDownloadEndpoint = (distributionOperatorEndpoint: string, dataObjectId: string) => {
-  return joinUrlFragments(distributionOperatorEndpoint, atlasConfig.storage.assetPath, dataObjectId)
+  try {
+    const url = operatorEndpoint[operatorEndpoint.length - 1] === '/' ? operatorEndpoint : operatorEndpoint + '/'
+    const uploadEndpoint = new URL(atlasConfig.storage.uploadPath, url)
+    Object.entries(uploadParams).forEach(([key, value]) => {
+      uploadEndpoint.searchParams.set(key, value)
+    })
+    return uploadEndpoint.toString()
+  } catch (error) {
+    throw new Error(error)
+  }
 }
 
 export const imageUrlValidation = async (imageUrl: string): Promise<boolean> =>

+ 8 - 34
packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.styles.ts

@@ -1,10 +1,9 @@
-import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 
-import { SvgControlsConnect, SvgLogoYoutube } from '@/assets/icons'
-import { SvgAppLogoShort, SvgAppLogoShortMonochrome } from '@/assets/logos'
+import { SvgAppLogoShort } from '@/assets/logos'
 import { Text } from '@/components/Text'
-import { cVar, sizes } from '@/styles'
+import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader'
+import { cVar, media, sizes } from '@/styles'
 
 export const StyledSvgAppLogoShort = styled(SvgAppLogoShort)`
   height: 36px;
@@ -17,13 +16,6 @@ export const StyledSvgAppLogoShort = styled(SvgAppLogoShort)`
 export const Content = styled.div`
   margin-top: ${sizes(6)};
 `
-export const AdditionalSubtitleWrapper = styled.div`
-  margin-top: ${sizes(6)};
-  margin-bottom: ${sizes(4)};
-`
-export const AdditionalSubtitle = styled(Text)`
-  display: inline;
-`
 
 export const DescriptionText = styled(Text)`
   display: block;
@@ -50,29 +42,11 @@ export const CategoriesText = styled(Text)`
   margin-top: ${sizes(1)};
 `
 
-export const LogosWrapper = styled.div`
-  display: grid;
-  grid-template-columns: repeat(3, auto);
-  justify-content: start;
-  align-items: center;
-  gap: ${sizes(4)};
-`
-
-const logoStyles = css`
-  path {
-    fill: ${cVar('colorTextMuted')};
-  }
-`
+export const RequirementsButtonSkeleton = styled(SkeletonLoader)`
+  height: 40px;
+  width: 100%;
 
-export const StyledSvgLogoYoutube = styled(SvgLogoYoutube)`
-  ${logoStyles};
-`
-export const StyledSvgControlsConnect = styled(SvgControlsConnect)`
-  path {
-    fill: ${cVar('colorCoreNeutral500')};
+  ${media.sm} {
+    width: 150px;
   }
 `
-// todo replace with AppLogo
-export const StyledAppLogo = styled(SvgAppLogoShortMonochrome)`
-  ${logoStyles};
-`

+ 64 - 74
packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx

@@ -11,6 +11,7 @@ import { SvgAlertsError32 } from '@/assets/icons'
 import appScreenshot from '@/assets/images/ypp-authorization/app-screenshot.webp'
 import { Text } from '@/components/Text'
 import { Button } from '@/components/_buttons/Button'
+import { GoogleButton } from '@/components/_buttons/GoogleButton'
 import { Loader } from '@/components/_loaders/Loader'
 import { DialogModal } from '@/components/_overlays/DialogModal'
 import { atlasConfig } from '@/config'
@@ -35,6 +36,7 @@ import {
   DescriptionText,
   HeaderIconsWrapper,
   Img,
+  RequirementsButtonSkeleton,
   StyledSvgAppLogoShort,
 } from './YppAuthorizationModal.styles'
 import { YppAuthorizationErrorCode, YppAuthorizationStepsType } from './YppAuthorizationModal.types'
@@ -75,12 +77,14 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
   const navigate = useNavigate()
   const [isSubmitting, setIsSubmitting] = useState(false)
   const { decrementOverlaysOpenCount } = useOverlayManager()
-  const { refetchYppSyncedChannels } = useGetYppSyncedChannels()
+  const {
+    unsyncedChannels: yppUnsyncedChannels,
+    currentChannel: yppCurrentChannel,
+    isLoading,
+  } = useGetYppSyncedChannels()
   const contentRef = useRef<HTMLDivElement | null>(null)
   const channelsLoaded = !!unSyncedChannels
-  const hasMoreThanOneChannel = unSyncedChannels && unSyncedChannels.length > 1
   const [finalFormData, setFinalFormData] = useState<FinalFormData | null>(null)
-  const [isFetchingData, setIsFetchingData] = useState(false)
   const selectedChannelId = useYppStore((store) => store.selectedChannelId)
   const referrerId = useYppStore((store) => store.referrerId)
   const setSelectedChannelId = useYppStore((store) => store.actions.setSelectedChannelId)
@@ -148,13 +152,9 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
       onChangeStep('ypp-sync')
     }
     if (currentStep === 'details' || currentStep === 'channel-already-registered') {
-      onChangeStep('requirements')
-    }
-    if (currentStep === 'requirements' && hasMoreThanOneChannel) {
-      setYtRequirementsErrors([])
       onChangeStep('select-channel')
     }
-  }, [currentStep, hasMoreThanOneChannel, onChangeStep, setYtRequirementsErrors])
+  }, [currentStep, onChangeStep])
 
   const handleSelectChannel = useCallback(
     (selectedChannelId: string) => {
@@ -326,66 +326,59 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
     [fetchedChannelRequirements, ytRequirementsErrors]
   )
 
-  const verifyChannelRequirements = useCallback(async () => {
-    const channels = activeMembership?.channels
-
-    if (!channels?.length) {
-      navigate(absoluteRoutes.studio.newChannel())
-      return
-    }
-    setIsFetchingData(true)
-    const { data } = await refetchYppSyncedChannels().finally(() => {
-      setIsFetchingData(false)
-    })
-    if (data?.currentChannel) {
-      navigate(absoluteRoutes.studio.ypp())
-      return
-    }
-
-    if (data?.unsyncedChannels?.length) {
-      setSelectedChannelId(data?.unsyncedChannels[0].id)
-    }
-    if (data?.unsyncedChannels?.length && data?.unsyncedChannels.length > 1) {
-      onChangeStep('select-channel')
-      return
-    }
-
-    handleAuthorizeClick(data?.unsyncedChannels?.[0].id)
-  }, [
-    activeMembership?.channels,
-    handleAuthorizeClick,
-    navigate,
-    onChangeStep,
-    refetchYppSyncedChannels,
-    setSelectedChannelId,
-  ])
-
   const authorizationStep = useMemo(() => {
     switch (currentStep) {
-      case 'requirements':
+      case 'requirements': {
+        const getPrimaryButton = () => {
+          if (isLoading) {
+            return <RequirementsButtonSkeleton />
+          }
+
+          if (yppCurrentChannel) {
+            navigate(absoluteRoutes.studio.ypp())
+          }
+
+          if (!activeMembership?.channels.length) {
+            return {
+              text: 'Create a new channel',
+              onClick: () => navigate(absoluteRoutes.studio.newChannel()),
+            }
+          }
+
+          if (yppUnsyncedChannels?.length) {
+            setSelectedChannelId(yppUnsyncedChannels[0].id)
+          }
+
+          if (yppUnsyncedChannels && yppUnsyncedChannels.length > 1) {
+            return {
+              text: 'Select channel',
+              onClick: () => onChangeStep('select-channel'),
+            }
+          }
+
+          return (
+            <GoogleButton
+              onClick={() => {
+                setSelectedChannelId(yppUnsyncedChannels?.[0].id ?? '')
+                handleAuthorizeClick(yppUnsyncedChannels?.[0].id)
+              }}
+            />
+          )
+        }
+
         return {
           title: 'Requirements',
           description: `Before you can apply to the program, make sure your YouTube channel meets the below conditions.`,
-          primaryButton: {
-            text: isFetchingData
-              ? 'Please wait...'
-              : activeMembership?.channels.length
-              ? 'Continue'
-              : 'Create new channel',
-            disabled: isFetchingData,
-            onClick: () => verifyChannelRequirements(),
-          },
+          primaryButton: getPrimaryButton(),
           component: <YppAuthorizationRequirementsStep requirements={requirements} />,
         }
+      }
+
       case 'select-channel':
         return {
           title: 'Select channel',
           description: `Select the ${APP_NAME} channel you want your YouTube channel to be connected with.`,
-          primaryButton: {
-            text: 'Authorize with YouTube',
-            onClick: () => handleAuthorizeClick,
-            disabled: !selectedChannel,
-          },
+          primaryButton: <GoogleButton disabled={!selectedChannel} onClick={() => handleAuthorizeClick()} />,
           component: (
             <YppAuthorizationSelectChannelStep
               channels={unSyncedChannels}
@@ -409,9 +402,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
           title: 'Details',
           description: 'Provide additional information to set up your program membership.',
           primaryButton: {
-            onClick: () => {
-              handleSubmitDetailsForm()
-            },
+            onClick: () => handleSubmitDetailsForm(),
             text: 'Continue',
           },
           component: <YppAuthorizationDetailsFormStep />,
@@ -421,9 +412,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
           title: 'YouTube Sync',
           description: `With YouTube Sync enabled, ${APP_NAME} will import videos from your YouTube channel over to Joystream. This can be changed later.`,
           primaryButton: {
-            onClick: () => {
-              handleSubmitDetailsForm()
-            },
+            onClick: () => handleSubmitDetailsForm(),
             text: 'Continue',
           },
           component: <YppAuthorizationSyncStep />,
@@ -460,10 +449,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
         return {
           headerIcon: <SvgAlertsError32 />,
           title: 'Authorization failed',
-          primaryButton: {
-            text: 'Select another channel',
-            onClick: () => handleAuthorizeClick,
-          },
+          primaryButton: <GoogleButton onClick={() => handleAuthorizeClick()} />,
           description: (
             <>
               The YouTube channel you selected is already enrolled in the YouTube Partner Program and is tied to the{' '}
@@ -480,10 +466,6 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
     }
   }, [
     currentStep,
-    isFetchingData,
-    activeMembership?.channels.length,
-    requirements,
-    handleAuthorizeClick,
     selectedChannel,
     unSyncedChannels,
     selectedChannelId,
@@ -493,12 +475,20 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
     handleGoToDashboard,
     alreadyRegisteredChannel?.channelTitle,
     alreadyRegisteredChannel?.ownerMemberHandle,
-    verifyChannelRequirements,
+    requirements,
+    isLoading,
+    yppCurrentChannel,
+    activeMembership?.channels.length,
+    yppUnsyncedChannels,
+    navigate,
+    setSelectedChannelId,
+    onChangeStep,
+    handleAuthorizeClick,
     handleSubmitDetailsForm,
   ])
 
   const secondaryButton = useMemo(() => {
-    if (currentStep === 'select-channel' || (currentStep === 'requirements' && !hasMoreThanOneChannel)) return
+    if (currentStep === 'requirements' || currentStep === 'select-channel') return
     if (currentStep === 'summary') {
       return {
         text: 'Close',
@@ -511,7 +501,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
       disabled: isSubmitting,
       onClick: handleGoBack,
     }
-  }, [currentStep, hasMoreThanOneChannel, handleGoBack, isSubmitting, handleClose])
+  }, [currentStep, handleGoBack, isSubmitting, handleClose])
 
   return (
     <FormProvider {...detailsFormMethods}>

+ 1 - 1
packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationRequirementsStep/YppAuthorizationRequirementsStep.styles.ts

@@ -4,7 +4,7 @@ import { Text } from '@/components/Text'
 import { cVar, sizes, square } from '@/styles'
 
 export const StyledList = styled.ul`
-  margin: 0;
+  margin: 0 0 ${sizes(6)} 0;
   padding-left: 0;
   display: grid;
   gap: ${sizes(4)};

+ 17 - 10
packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationRequirementsStep/YppAuthorizationRequirementsStep.tsx

@@ -1,6 +1,8 @@
 import { FC, ReactNode } from 'react'
 
 import { SvgActionCheck, SvgActionClose } from '@/assets/icons'
+import { Banner } from '@/components/Banner'
+import { atlasConfig } from '@/config'
 
 import { ListItem, Paragraph, StyledList, TickWrapper } from './YppAuthorizationRequirementsStep.styles'
 
@@ -15,15 +17,20 @@ type YppAuthorizationRequirementsStepProps = {
 
 export const YppAuthorizationRequirementsStep: FC<YppAuthorizationRequirementsStepProps> = ({ requirements }) => {
   return (
-    <StyledList>
-      {requirements.map((item, idx) => (
-        <ListItem key={idx} as="li" variant="t200" color="colorText">
-          <TickWrapper fulfilled={item.fulfilled}>
-            {item.fulfilled ? <SvgActionCheck /> : <SvgActionClose />}
-          </TickWrapper>
-          <Paragraph>{item.text}</Paragraph>
-        </ListItem>
-      ))}
-    </StyledList>
+    <>
+      <StyledList>
+        {requirements.map((item, idx) => (
+          <ListItem key={idx} as="li" variant="t200" color="colorText">
+            <TickWrapper fulfilled={item.fulfilled}>
+              {item.fulfilled ? <SvgActionCheck /> : <SvgActionClose />}
+            </TickWrapper>
+            <Paragraph>{item.text}</Paragraph>
+          </ListItem>
+        ))}
+      </StyledList>
+      <Banner
+        description={`${atlasConfig.general.appName} uses Google OAuth to get access to your public profile and account email address as part of sign up flow, and integrates with YouTube API to obtain details about your YouTube channel data, such as followers and video statistics.`}
+      />
+    </>
   )
 }

+ 2 - 19
packages/atlas/src/views/global/YppLandingView/YppFooter.styles.ts

@@ -3,7 +3,7 @@ import styled from '@emotion/styled'
 import bottomLeftPattern from '@/assets/images/ypp-background-pattern.svg'
 import topLeftBannerPattern from '@/assets/images/ypp-banner-pattern.svg'
 import { Text } from '@/components/Text'
-import { Button } from '@/components/_buttons/Button'
+import { GoogleButton } from '@/components/_buttons/GoogleButton'
 import { cVar, media, sizes } from '@/styles'
 
 export const CtaBanner = styled.div`
@@ -35,23 +35,6 @@ export const StyledBannerText = styled(Text)`
   }
 `
 
-export const StyledButton = styled(Button)`
+export const StyledButton = styled(GoogleButton)`
   margin-top: ${sizes(8)};
-  background-color: ${cVar('colorCoreBaseBlack')};
-`
-
-export const CtaCardRow = styled.div<{ itemsCount: number }>`
-  display: grid;
-  gap: ${sizes(4)};
-  padding-bottom: ${sizes(16)};
-  justify-items: center;
-
-  ${media.md} {
-    margin: 0 auto;
-    justify-content: center;
-    grid-template-columns: repeat(auto-fit, minmax(219px, 1fr));
-    max-width: ${({ itemsCount }) => `calc(${itemsCount - 1} * ${sizes(6)} + 419px * ${itemsCount}) `};
-    gap: ${sizes(6)};
-    padding-bottom: ${sizes(24)};
-  }
 `

+ 6 - 8
packages/atlas/src/views/global/YppLandingView/YppFooter.tsx

@@ -1,14 +1,14 @@
 import { FC, ReactElement } from 'react'
 
-import { SvgActionChevronR, SvgActionInfo, SvgActionSpeech, SvgActionTokensStack } from '@/assets/icons'
+import { SvgActionInfo, SvgActionSpeech, SvgActionTokensStack } from '@/assets/icons'
 import { GridItem, LayoutGrid } from '@/components/LayoutGrid'
 import { Text } from '@/components/Text'
-import { CallToActionButton } from '@/components/_buttons/CallToActionButton'
+import { CallToActionButton, CallToActionWrapper } from '@/components/_buttons/CallToActionButton'
 import { atlasConfig } from '@/config'
 import { YppWidgetIcons } from '@/config/configSchema'
 import { useMediaMatch } from '@/hooks/useMediaMatch'
 
-import { CtaBanner, CtaCardRow, StyledBannerText, StyledButton } from './YppFooter.styles'
+import { CtaBanner, StyledBannerText, StyledButton } from './YppFooter.styles'
 import { StyledLimitedWidthContainer } from './YppLandingView.styles'
 
 export const configYppIconMapper: Record<YppWidgetIcons, ReactElement> = {
@@ -47,15 +47,13 @@ export const YppFooter: FC<YppFooterSectionProps> = ({ onSignUpClick }) => {
                 Get the most out of your YouTube channel
               </StyledBannerText>
 
-              <StyledButton size="large" icon={<SvgActionChevronR />} onClick={onSignUpClick} iconPlacement="right">
-                Authorize with YouTube
-              </StyledButton>
+              <StyledButton onClick={onSignUpClick}>Authorize with YouTube</StyledButton>
             </CtaBanner>
           </GridItem>
         </LayoutGrid>
       </StyledLimitedWidthContainer>
       {atlasConfig.features.ypp.widgets && (
-        <CtaCardRow itemsCount={atlasConfig.features.ypp.widgets.length}>
+        <CallToActionWrapper itemsCount={atlasConfig.features.ypp.widgets.length}>
           {atlasConfig.features.ypp.widgets.map((widget) => (
             <CallToActionButton
               icon={widget.icon && configYppIconMapper[widget.icon]}
@@ -66,7 +64,7 @@ export const YppFooter: FC<YppFooterSectionProps> = ({ onSignUpClick }) => {
               to={widget.link}
             />
           ))}
-        </CtaCardRow>
+        </CallToActionWrapper>
       )}
     </>
   )

+ 8 - 0
packages/atlas/src/views/global/YppLandingView/YppHero.styles.ts

@@ -19,6 +19,14 @@ export const ButtonWrapper = styled.div`
   justify-content: center;
 `
 
+export const LogosContainer = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: ${sizes(10)};
+  margin-bottom: ${sizes(8)};
+`
+
 export const SelectDifferentChannelButton = styled.button`
   white-space: normal;
   border: none;

+ 20 - 22
packages/atlas/src/views/global/YppLandingView/YppHero.tsx

@@ -2,7 +2,7 @@ import { FC } from 'react'
 import { useParallax } from 'react-scroll-parallax'
 import { CSSTransition, SwitchTransition } from 'react-transition-group'
 
-import { SvgActionChevronR } from '@/assets/icons'
+import { SvgActionChevronR, SvgLogoGoogleWhiteFull, SvgLogoYoutubeWhiteFull } from '@/assets/icons'
 import hero576 from '@/assets/images/ypp-hero/hero-576.webp'
 import hero864 from '@/assets/images/ypp-hero/hero-864.webp'
 import hero1152 from '@/assets/images/ypp-hero/hero-1152.webp'
@@ -14,6 +14,7 @@ import yt2304 from '@/assets/images/ypp-hero/yt-2304.webp'
 import { GridItem, LayoutGrid } from '@/components/LayoutGrid'
 import { Text } from '@/components/Text'
 import { Button } from '@/components/_buttons/Button'
+import { GoogleButton } from '@/components/_buttons/GoogleButton'
 import { ChannelCard } from '@/components/_channel/ChannelCard'
 import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader'
 import { atlasConfig } from '@/config'
@@ -25,6 +26,7 @@ import {
   ButtonWrapper,
   FrontImage,
   HeroImageWrapper,
+  LogosContainer,
   SelectDifferentChannelButton,
   StyledInfiniteCarousel,
 } from './YppHero.styles'
@@ -41,18 +43,6 @@ type YppHeroProps = {
   selectedChannelTitle?: string | null
 }
 
-export const getButtonText = (variant: YppAtlasStatus) => {
-  switch (variant) {
-    case 'have-channel':
-      return 'Sign up now'
-    case 'connect-wallet':
-    case 'no-channel':
-      return 'Create channel & sign up'
-    case 'ypp-signed':
-      return 'Go to dashboard'
-  }
-}
-
 export const YppHero: FC<YppHeroProps> = ({
   onSignUpClick,
   onSelectChannel,
@@ -104,6 +94,10 @@ export const YppHero: FC<YppHeroProps> = ({
             >
               Reupload and backup your YouTube videos to receive a guaranteed payout in the YouTube Partner Program.
             </Text>
+            <LogosContainer data-aos="fade-up" data-aos-delay="350" data-aos-offset="40" data-aos-easing="atlas-easing">
+              <SvgLogoYoutubeWhiteFull />
+              <SvgLogoGoogleWhiteFull />
+            </LogosContainer>
             <ButtonWrapper data-aos="fade-up" data-aos-delay="450" data-aos-offset="40" data-aos-easing="atlas-easing">
               <SwitchTransition>
                 <CSSTransition
@@ -112,15 +106,19 @@ export const YppHero: FC<YppHeroProps> = ({
                   classNames={transitions.names.fade}
                 >
                   {yppAtlasStatus ? (
-                    <Button
-                      size="large"
-                      variant={yppAtlasStatus === 'ypp-signed' ? 'secondary' : 'primary'}
-                      icon={<SvgActionChevronR />}
-                      iconPlacement="right"
-                      onClick={onSignUpClick}
-                    >
-                      {getButtonText(yppAtlasStatus)}
-                    </Button>
+                    yppAtlasStatus === 'ypp-signed' ? (
+                      <Button
+                        size="large"
+                        variant="secondary"
+                        icon={<SvgActionChevronR />}
+                        iconPlacement="right"
+                        onClick={onSignUpClick}
+                      >
+                        Go to dashboard
+                      </Button>
+                    ) : (
+                      <GoogleButton onClick={onSignUpClick} />
+                    )
                   ) : (
                     <SkeletonLoader width={190} height={48} />
                   )}

+ 25 - 14
packages/atlas/src/views/global/YppLandingView/YppThreeStepsSection.tsx

@@ -6,9 +6,10 @@ import memberDropdown from '@/assets/images/member-dropdown.webp'
 import selectChannel from '@/assets/images/select-channel.webp'
 import { Text } from '@/components/Text'
 import { Button } from '@/components/_buttons/Button'
+import { GoogleButton } from '@/components/_buttons/GoogleButton'
 import { atlasConfig } from '@/config'
 import { useMediaMatch } from '@/hooks/useMediaMatch'
-import { YppAtlasStatus, getButtonText } from '@/views/global/YppLandingView/YppHero'
+import { YppAtlasStatus } from '@/views/global/YppLandingView/YppHero'
 
 import {
   BackgroundContainer,
@@ -54,19 +55,29 @@ export const YppThreeStepsSection: FC<YppThreeStepsSectionProps> = ({ onSignUpCl
               Our fully automated verification process is as simple as 1-2-3. If you don't have an {appName} channel
               already, you'll be able to create one for free.
             </Text>
-            <Button
-              size="large"
-              variant={yppStatus === 'ypp-signed' ? 'secondary' : 'primary'}
-              iconPlacement="right"
-              icon={<SvgActionChevronR />}
-              data-aos="fade-up"
-              data-aos-delay="200"
-              data-aos-offset="40"
-              data-aos-easing="atlas-easing"
-              onClick={onSignUpClick}
-            >
-              {getButtonText(yppStatus)}
-            </Button>
+            {yppStatus === 'ypp-signed' ? (
+              <Button
+                size="large"
+                variant="secondary"
+                iconPlacement="right"
+                icon={<SvgActionChevronR />}
+                data-aos="fade-up"
+                data-aos-delay="200"
+                data-aos-offset="40"
+                data-aos-easing="atlas-easing"
+                onClick={onSignUpClick}
+              >
+                Go to dashboard
+              </Button>
+            ) : (
+              <GoogleButton
+                data-aos="fade-up"
+                data-aos-delay="200"
+                data-aos-offset="40"
+                data-aos-easing="atlas-easing"
+                onClick={onSignUpClick}
+              />
+            )}
             <Text
               variant="t100"
               as="p"

+ 1 - 1
packages/atlas/src/views/playground/PlaygroundLayout.tsx

@@ -26,13 +26,13 @@ import {
   PlaygroundImageDownsizing,
   PlaygroundIndirectSignInDialog,
   PlaygroundInputAutocomplete,
-  PlaygroundMarketplaceCarousel,
   PlaygroundNftPurchase,
   PlaygroundNftSettleAuction,
   PlaygroundNftWhitelistMembers,
   PlaygroundReactionsComments,
   PlaygroundTokenPrice,
 } from './Playgrounds'
+import { PlaygroundMarketplaceCarousel } from './Playgrounds/PlaygroundMarketplaceCarousel'
 
 const playgroundRoutes = [
   { path: 'nft-purchase', element: <PlaygroundNftPurchase />, name: 'NFT Purchase' },

+ 0 - 1
packages/atlas/src/views/playground/Playgrounds/index.ts

@@ -10,4 +10,3 @@ export * from './PlaygroundIframe'
 export * from './PlaygroundCaptcha'
 export * from './PlaygroundGoogleAuthentication'
 export * from './PlaygroundInputAutocomplete'
-export * from './PlaygroundMarketplaceCarousel'

+ 7 - 1
packages/atlas/src/views/studio/MyPaymentsView/PaymentsOverview/PaymentsOverview.hooks.ts

@@ -91,7 +91,13 @@ export const useChannelPayout = (txCallback?: () => void) => {
         }
         return { reward, payloadUrl, commitment }
       } catch (error) {
-        SentryLogger.error("Couldn't get reward data", 'PaymentOverviewTab.hooks', error)
+        const errorMessage = error?.message
+        if (typeof errorMessage === 'string' && errorMessage.startsWith('No payout Proof exists for channel')) {
+          // This error will experience every user that don't have claimable reward. No need to send this to sentry or log it.
+          return
+        } else {
+          SentryLogger.error("Couldn't get reward data", 'PaymentOverviewTab.hooks', error)
+        }
       }
     },
     [getPayloadUrl, joystream, maxCashoutAllowed, minCashoutAllowed]

+ 1 - 1
packages/atlas/src/views/viewer/CategoryView/CategoryView.tsx

@@ -49,7 +49,7 @@ export const CategoryView = () => {
   const videoHeroVideos = useVideoHeroVideos(categoriesFeaturedVideos)
 
   return (
-    <VideoContentTemplate cta={['popular', 'new', 'home']}>
+    <VideoContentTemplate>
       {headTags}
       <VideoCategoryHero
         loading={categoriesFeaturedVideosLoading}

+ 37 - 0
packages/atlas/src/views/viewer/ChannelView/ChannelView.styles.ts

@@ -1,6 +1,7 @@
 import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 
+import { SvgJoyTokenMonochrome16 } from '@/assets/icons'
 import { Tabs } from '@/components/Tabs'
 import { Text } from '@/components/Text'
 import { Button } from '@/components/_buttons/Button'
@@ -47,6 +48,30 @@ export const TitleContainer = styled.div`
   }
 `
 
+export const ChannelInfoContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: ${sizes(2)};
+  margin: ${sizes(2)} 0;
+
+  .divider-dot {
+    display: none;
+  }
+
+  p {
+    margin: 0;
+  }
+
+  ${media.xs} {
+    flex-direction: row;
+
+    .divider-dot {
+      display: block;
+    }
+  }
+`
+
 export const StyledSelect = styled(Select)`
   grid-area: sort;
 
@@ -69,6 +94,18 @@ export const SubTitle = styled(Text)`
   display: inline-block;
 `
 
+export const Balance = styled(Text)`
+  display: flex;
+  align-items: center;
+  gap: 4px;
+`
+
+export const SvgToken = styled(SvgJoyTokenMonochrome16)`
+  path {
+    fill: ${cVar('colorText')};
+  }
+`
+
 export const StyledChannelLink = styled(ChannelLink)`
   position: relative;
   width: fit-content;

+ 35 - 8
packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx

@@ -25,12 +25,15 @@ import { useHandleFollowChannel } from '@/hooks/useHandleFollowChannel'
 import { useHeadTags } from '@/hooks/useHeadTags'
 import { useMediaMatch } from '@/hooks/useMediaMatch'
 import { useVideoGridRows } from '@/hooks/useVideoGridRows'
+import { useSubscribeAccountBalance } from '@/providers/joystream/joystream.hooks'
 import { transitions } from '@/styles'
 import { SentryLogger } from '@/utils/logs'
 
 import { ChannelSearch } from './ChannelSearch'
 import { useSearchVideos } from './ChannelView.hooks'
 import {
+  Balance,
+  ChannelInfoContainer,
   CollectorsBoxContainer,
   FilterButton,
   NotFoundChannelContainer,
@@ -41,6 +44,7 @@ import {
   StyledTabs,
   SubTitle,
   SubTitleSkeletonLoader,
+  SvgToken,
   TabsContainer,
   TabsWrapper,
   TitleContainer,
@@ -102,6 +106,7 @@ export const ChannelView: FC = () => {
         search: { channelId: id, query: searchQuery },
       }),
   })
+  const { accountBalance } = useSubscribeAccountBalance(channel?.rewardAccount)
   const { channelNftCollectors } = useChannelNftCollectors({ channelId: id || '' })
 
   const { toggleFollowing, isFollowing } = useHandleFollowChannel(id, channel?.title)
@@ -244,14 +249,36 @@ export const ChannelView: FC = () => {
                 <Text as="h1" variant={smMatch ? 'h700' : 'h600'}>
                   {channel.title}
                 </Text>
-                <SubTitle as="p" variant="t300" color="colorText">
-                  {channel.followsNum ? (
-                    <NumberFormat as="span" value={channel.followsNum} format="short" variant="t300" />
-                  ) : (
-                    0
-                  )}{' '}
-                  Followers
-                </SubTitle>
+                <ChannelInfoContainer>
+                  <SubTitle as="p" variant="t300" color="colorText">
+                    {channel.followsNum ? (
+                      <NumberFormat
+                        as="span"
+                        value={channel.followsNum}
+                        format="short"
+                        color="colorText"
+                        variant="t300"
+                      />
+                    ) : (
+                      0
+                    )}{' '}
+                    Followers
+                  </SubTitle>
+                  <SubTitle className="divider-dot" as="p" variant="t300" color="colorText">
+                    •
+                  </SubTitle>
+                  <Balance as="span" variant="t300" color="colorText">
+                    Balance:
+                    <SvgToken />
+                    <NumberFormat
+                      as="span"
+                      value={accountBalance ?? 0}
+                      format="short"
+                      color="colorText"
+                      variant="t300"
+                    />
+                  </Balance>
+                </ChannelInfoContainer>
               </>
             ) : (
               <>

+ 3 - 1
packages/atlas/src/views/viewer/ChannelsView/ChannelsView.tsx

@@ -2,6 +2,7 @@ import { FC } from 'react'
 
 import { useTop10Channels } from '@/api/hooks/channel'
 import { ChannelGallery } from '@/components/_channel/ChannelGallery'
+import { ChannelsSection } from '@/components/_channel/ChannelsSection'
 import { ExpandableChannelsList } from '@/components/_channel/ExpandableChannelsList'
 import { DiscoverChannels } from '@/components/_content/DiscoverChannels'
 import { VideoContentTemplate } from '@/components/_templates/VideoContentTemplate'
@@ -18,10 +19,11 @@ export const ChannelsView: FC = () => {
   return (
     <>
       {headTags}
-      <VideoContentTemplate title="Browse channels" cta={['popular', 'new', 'home']}>
+      <VideoContentTemplate title="Browse channels">
         {!error ? <ChannelGallery hasRanking channels={channels} loading={loading} title="Top 10 channels" /> : null}
         <DiscoverChannels />
         <ExpandableChannelsList queryType="regular" title="Channels in your language" languageSelector />
+        <ChannelsSection />
       </VideoContentTemplate>
     </>
   )

+ 1 - 1
packages/atlas/src/views/viewer/HomeView.tsx

@@ -53,7 +53,7 @@ export const HomeView: FC = () => {
   }
 
   return (
-    <VideoContentTemplate cta={['popular', 'new', 'channels']}>
+    <VideoContentTemplate>
       {headTags}
       <VideoHero videoHeroData={videoHero} withMuteButton loading={loading} />
       <Container className={transitions.names.slide}>

+ 0 - 23
packages/atlas/src/views/viewer/NewView/NewView.tsx

@@ -1,23 +0,0 @@
-import { FC } from 'react'
-
-import { InfiniteVideoGrid } from '@/components/InfiniteGrids'
-import { ExpandableChannelsList } from '@/components/_channel/ExpandableChannelsList'
-import { VideoContentTemplate } from '@/components/_templates/VideoContentTemplate'
-import { absoluteRoutes } from '@/config/routes'
-import { useHeadTags } from '@/hooks/useHeadTags'
-
-export const NewView: FC = () => {
-  const headTags = useHeadTags('New & Noteworthy')
-
-  return (
-    <VideoContentTemplate title="New & Noteworthy" cta={['home', 'channels', 'popular']}>
-      {headTags}
-      <InfiniteVideoGrid title="Recently uploaded" onDemand />
-      <ExpandableChannelsList
-        title="Promising new channels"
-        queryType="promising"
-        additionalLink={{ name: 'Browse channels', url: absoluteRoutes.viewer.channels() }}
-      />
-    </VideoContentTemplate>
-  )
-}

+ 0 - 1
packages/atlas/src/views/viewer/NewView/index.ts

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

+ 3 - 143
packages/atlas/src/views/viewer/NftsView/NftsView.tsx

@@ -1,156 +1,16 @@
-import { FC, useState } from 'react'
+import { FC } from 'react'
 
-import { useNfts } from '@/api/hooks/nfts'
-import { OwnedNftOrderByInput, OwnedNftWhereInput } from '@/api/queries/__generated__/baseTypes.generated'
-import { SvgActionFilters } from '@/assets/icons'
-import { EmptyFallback } from '@/components/EmptyFallback'
-import { FiltersBar, useFiltersBar } from '@/components/FiltersBar'
-import { GridItem } from '@/components/LayoutGrid'
-import { Text } from '@/components/Text'
-import { Button } from '@/components/_buttons/Button'
-import { Select } from '@/components/_inputs/Select'
-import { NftTileViewer } from '@/components/_nft/NftTileViewer'
+import { AllNftSection } from '@/components/AllNftSection/AllNftSection'
 import { VideoContentTemplate } from '@/components/_templates/VideoContentTemplate'
 import { useHeadTags } from '@/hooks/useHeadTags'
-import { useMediaMatch } from '@/hooks/useMediaMatch'
-import { useVideoGridRows } from '@/hooks/useVideoGridRows'
-import { createPlaceholderData } from '@/utils/data'
-import { SentryLogger } from '@/utils/logs'
-import { StyledPagination } from '@/views/studio/MyVideosView/MyVideos.styles'
-
-import { HeaderContainer, HeaderWrapper, StyledGrid } from './NftsView.styles'
-
-const SORT_OPTIONS = [
-  { name: 'newest', value: OwnedNftOrderByInput.CreatedAtDesc },
-  { name: 'oldest', value: OwnedNftOrderByInput.CreatedAtAsc },
-]
-
-const VIEWER_TIMESTAMP = new Date()
 
 export const NftsView: FC = () => {
   const headTags = useHeadTags('Video NFTs')
-  const smMatch = useMediaMatch('sm')
-  const mdMatch = useMediaMatch('md')
-  const filtersBarLogic = useFiltersBar()
-  const {
-    filters: { setIsFiltersOpen, isFiltersOpen },
-    canClearFilters: { canClearAllFilters },
-    ownedNftWhereInput,
-    videoWhereInput,
-  } = filtersBarLogic
-
-  const [sortBy, setSortBy] = useState<OwnedNftOrderByInput>(OwnedNftOrderByInput.CreatedAtDesc)
-  const [currentPage, setCurrentPage] = useState(0)
-  const [tilesPerRow, setTilesPerRow] = useState(4)
-  const nftRows = useVideoGridRows('main')
-  const tilesPerPage = nftRows * tilesPerRow
-
-  const basicVariables: OwnedNftWhereInput = {
-    createdAt_lte: VIEWER_TIMESTAMP,
-    createdAt_gte: videoWhereInput.createdAt_gte,
-    video: {
-      ...videoWhereInput,
-      media: {
-        isAccepted_eq: true,
-      },
-      thumbnailPhoto: {
-        isAccepted_eq: true,
-      },
-      isPublic_eq: true,
-      channel: {
-        isPublic_eq: true,
-      },
-    },
-  }
-
-  const orVariablesFromFilter = ownedNftWhereInput.OR?.map((value) => ({
-    ...basicVariables,
-    ...value,
-  }))
-
-  const { nfts, loading, totalCount } = useNfts({
-    variables: {
-      where: {
-        ...(orVariablesFromFilter?.length
-          ? { OR: orVariablesFromFilter }
-          : { ...ownedNftWhereInput, ...basicVariables }),
-      },
-      orderBy: sortBy,
-      limit: tilesPerPage,
-      offset: currentPage * tilesPerPage,
-    },
-    fetchPolicy: 'cache-first',
-    notifyOnNetworkStatusChange: true,
-    onError: (error) => SentryLogger.error('Failed to fetch NFTs', 'NftsView', error),
-  })
-
-  const handleResizeGrid = (sizes: number[]) => setTilesPerRow(sizes.length)
-
-  const handleSortingChange = (value?: OwnedNftOrderByInput | null) => {
-    if (value) {
-      setSortBy(value)
-    }
-  }
-
-  const handleFilterClick = () => {
-    setIsFiltersOpen((value) => !value)
-  }
-
-  const handleChangePage = (page: number) => {
-    setCurrentPage(page)
-  }
-
-  const sortingNode = (
-    <Select size="medium" value={sortBy} inlineLabel="Sort by" items={SORT_OPTIONS} onChange={handleSortingChange} />
-  )
 
   return (
     <VideoContentTemplate title="Video NFTs">
       {headTags}
-      <HeaderWrapper>
-        <HeaderContainer>
-          <GridItem colSpan={{ base: 2, sm: 1 }}>
-            <Text as="p" variant={mdMatch ? 'h500' : 'h400'}>
-              All NFTs {totalCount !== undefined && `(${totalCount})`}
-            </Text>
-          </GridItem>
-          {!smMatch && sortingNode}
-          <div>
-            <Button
-              badge={canClearAllFilters}
-              variant="secondary"
-              icon={<SvgActionFilters />}
-              onClick={handleFilterClick}
-            >
-              Filters
-            </Button>
-          </div>
-          {smMatch && sortingNode}
-        </HeaderContainer>
-        <FiltersBar
-          {...filtersBarLogic}
-          onAnyFilterSet={() => setCurrentPage(0)}
-          activeFilters={['nftStatus', 'date-minted', 'other']}
-        />
-      </HeaderWrapper>
-      <StyledGrid maxColumns={null} onResize={handleResizeGrid} isFiltersOpen={isFiltersOpen}>
-        {(loading ? createPlaceholderData(tilesPerPage) : nfts ?? []).map((nft, idx) => (
-          <NftTileViewer key={`${idx}-${nft.id}`} nftId={nft.id} />
-        ))}
-      </StyledGrid>
-      {nfts && !nfts.length && (
-        <EmptyFallback
-          title="No NFTs found"
-          subtitle={canClearAllFilters ? 'Try changing the filters.' : 'No NFTs were minted yet.'}
-          variant="large"
-        />
-      )}
-      <StyledPagination
-        onChangePage={handleChangePage}
-        page={currentPage}
-        itemsPerPage={tilesPerPage}
-        totalCount={totalCount}
-      />
+      <AllNftSection />
     </VideoContentTemplate>
   )
 }

+ 1 - 3
packages/atlas/src/views/viewer/PopularView/PopularView.tsx

@@ -8,16 +8,14 @@ import { VideoContentTemplate } from '@/components/_templates/VideoContentTempla
 import { atlasConfig } from '@/config'
 import { absoluteRoutes } from '@/config/routes'
 import { useHeadTags } from '@/hooks/useHeadTags'
-import { CtaData } from '@/types/cta'
 
-const CTA: CtaData[] = ['new', 'home', 'channels']
 const ADDITIONAL_LINK = { name: 'Browse channels', url: absoluteRoutes.viewer.channels() }
 
 export const PopularView: FC = () => {
   const headTags = useHeadTags('Popular')
 
   return (
-    <VideoContentTemplate title={`Popular on ${atlasConfig.general.appName}`} cta={CTA}>
+    <VideoContentTemplate title={`Popular on ${atlasConfig.general.appName}`}>
       {headTags}
       <TopTenVideos period="month" />
       <InfiniteVideoGrid title="Popular videos" query={GetMostViewedVideosConnectionDocument} limit={50} onDemand />

+ 2 - 2
packages/atlas/src/views/viewer/VideoView/VideoView.tsx

@@ -434,8 +434,8 @@ export const VideoView: FC = () => {
             {sideItems}
           </LayoutGrid>
         )}
-        <StyledCallToActionWrapper>
-          {['popular', 'new', 'discover'].map((item, idx) => (
+        <StyledCallToActionWrapper itemsCount={2}>
+          {['popular', 'discover'].map((item, idx) => (
             <CallToActionButton key={`cta-${idx}`} {...CTA_MAP[item]} />
           ))}
         </StyledCallToActionWrapper>

+ 0 - 2
packages/atlas/src/views/viewer/ViewerLayout.tsx

@@ -26,7 +26,6 @@ import { DiscoverView } from './DiscoverView'
 import { EditMembershipView } from './EditMembershipView'
 import { HomeView } from './HomeView'
 import { MemberView } from './MemberView'
-import { NewView } from './NewView'
 import { NftsView } from './NftsView'
 import { NotFoundView } from './NotFoundView'
 import { PopularView } from './PopularView'
@@ -37,7 +36,6 @@ const viewerRoutes = [
   { path: relativeRoutes.viewer.search(), element: <SearchView /> },
   { path: relativeRoutes.viewer.index(), element: <HomeView /> },
   { path: relativeRoutes.viewer.popular(), element: <PopularView /> },
-  { path: relativeRoutes.viewer.new(), element: <NewView /> },
   { path: relativeRoutes.viewer.discover(), element: <DiscoverView /> },
   { path: relativeRoutes.viewer.video(), element: <VideoView /> },
   { path: relativeRoutes.viewer.channels(), element: <ChannelsView /> },

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff