Skip to content

Introduction to web3 social media with Next.js and Lens Protocol

Notifications You must be signed in to change notification settings

dabit3/lens-protocol-workshop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 

Repository files navigation

Lens Protocol Workshop

In this workshop you'll learn how to use Next.js, TypeScript, Lens Protocol, and the Lens SDK to build out a social application.

The app we'll be building will have the following features:

  1. When the app loads, it will render a list of recommended users from the Lens API along with their profile picture and bio
  2. When we click on a user, we will navigate to a detail view where we will see all of their publications as well as more profile details
  3. The user profile view will also have the option for a user to sign in and follow another user.

By the end of this tutorial you'll have a good understanding of how to get started building on Lens with TypeScript and the Lens SDK.

Lens Dev Stack

Getting started

To get started, create a new Next.js application:

npx create-next-app lens-app

âś” Would you like to use TypeScript with this project? Yes
âś” Would you like to use ESLint with this project? Yes
âś” Would you like to use Tailwind CSS with this project?  Yes
âś” Would you like to use `src/` directory with this project? No
âś” Use App Router (recommended)? Yes
âś” Would you like to customize the default import alias? No

Next, change into the new directory and install the following dependencies:

npm install @lens-protocol/react-web@alpha @lens-protocol/wagmi@alpha ethers@legacy-v5 wagmi viem @web3modal/wagmi

Next, update the tsconfig.json and add the following to the compilerOptions configuration:

"noImplicitAny": false,

WalletConnect Setup

Go to https://cloud.walletconnect.com/app and create a project, then copy the Project ID.

Create a file named .env.local in the root directory, adding the following code:

NEXT_PUBLIC_WC_ID="<your-project-id>"

Web3Modal Provider

Create a file in the app directory named Web3ModalProvider.tsx and add the following code:

"use client"

import { createWeb3Modal, defaultWagmiConfig } from '@web3modal/wagmi/react'

import { WagmiConfig } from 'wagmi'
import { arbitrum, mainnet } from 'viem/chains'

const projectId = process.env.NEXT_PUBLIC_WC_ID || ''

const metadata = {
  name: 'Web3Modal',
  description: 'Web3Modal Example',
  url: 'https://web3modal.com',
  icons: ['https://avatars.githubusercontent.com/u/37784886']
}

const chains = [mainnet, arbitrum]
const wagmiConfig = defaultWagmiConfig({ chains, projectId, metadata })

createWeb3Modal({ wagmiConfig, projectId, chains })

export function Web3ModalProvider({ children }) {
  return <WagmiConfig config={wagmiConfig}>{children}</WagmiConfig>
}

Lens Provider

Next, create a file in the app directory named LensProvider.tsx and add the following code:

'use client'
import { LensConfig, production } from '@lens-protocol/react-web'
import { bindings as wagmiBindings } from '@lens-protocol/wagmi'
import { LensProvider as Provider } from '@lens-protocol/react-web'

const lensConfig: LensConfig = {
  bindings: wagmiBindings(),
  environment: production,
}

export function LensProvider({ children }) {
  return (
    <Provider config={lensConfig}>
      { children }
    </Provider>
  )
}

app/layout.tsx

Next, we want to configure our app to use the providers.

This is typically done at the entrypoint to the app, and only needs to be done once.

Update app/layout.tsx with the following code:

// app/layout.tsx
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { LensProvider } from './LensProvider'
import { Web3ModalProvider } from './Web3ModalProvider'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Web3ModalProvider>
          <LensProvider>
            {children}
          </LensProvider>
        </Web3ModalProvider>
      </body>
    </html>
  )
}

app/page.tsx

Next, let's query for profiles and render them in our app.

To do so, open app/page.tsx and add the following code:

// app/page.tsx
'use client'
import Image from 'next/image'
import { useExploreProfiles, ExploreProfilesOrderByType } from '@lens-protocol/react-web'
import Link from 'next/link'

export default function Home() {
  const { data } = useExploreProfiles({
    orderBy: ExploreProfilesOrderByType.MostFollowers
  })
  
  return (
    <div className='p-20'>
      <h1 className='text-5xl'>My Lens App</h1>
      {
        data?.map((profile, index) => (
          <Link
            href={`/profile/${profile.handle?.localName}.${profile.handle?.namespace}`}
            key={index}
          >
            <div className='my-14'>
              {
                profile.metadata?.picture?.__typename === 'ImageSet' ? (
                  <img
                    src={profile.metadata?.picture?.optimized?.uri || ''}
                    width="120"
                    height="120"
                    alt={profile.handle?.fullHandle || ''}
                  />
                ) : <div className="w-28 h-28 bg-slate-500" />
              }
              <h3 className="text-3xl my-4">{profile.handle?.localName}.{profile.handle?.namespace}</h3>
              <p className="text-xl">{profile.metadata?.bio}</p>
            </div>
          </Link>
        ))
      }
    </div>
  )
}

What's happening?

In useExploreProfiles, we are calling the Lens API to fetch a list of recommended profiles.

Once the profiles are returned, we map over them, rendering each profile with their profile details and a link to view the individual profile which we'll build out later.

Testing it out

To run the app, run the following command:

npm run dev

Profile View

In the above code, we've added a link to each profile that, when clicked, will navigate to /profile/profile.id. What we want to happen is that when a user navigates to that page, they are able to view more details about that profile.

We also want go give users a way to sign in and follow users.

This functionality does not yet exist, so let's create it.

In the app directory, create a new folder named profile.

In the profile directory create a new folder named [handle].

In the [handle] folder, create a new file named page.tsx.

In this file, add the following code:

// app/profile/[handle]/page.tsx
'use client'
import {
  useProfile, usePublications, Profile, LimitType, PublicationType
} from '@lens-protocol/react-web'

export default function Profile({ params: { handle }}) {
  const namespace = handle.split('.')[1]
  handle = handle.split('.')[0]
  let { data: profile, loading } = useProfile({
    forHandle: `${namespace}/${handle}`
  })
  if (loading) return <p className="p-14">Loading ...</p>

  return (
    <div>
      <div className="p-14">
        {
          profile?.metadata?.picture?.__typename === 'ImageSet' && (
            <img
              width="200"
              height="200"
              alt={profile.handle?.fullHandle}
              className='rounded-xl'
              src={profile.metadata.picture.optimized?.uri}
            />
          )
        }
        <h1 className="text-3xl my-3">{profile?.handle?.localName}.{profile?.handle?.namespace}</h1>
        <h3 className="text-xl mb-4">{profile?.metadata?.bio}</h3>
       { profile && <Publications profile={profile} />}
      </div>
    </div>
  )
}

function Publications({
  profile
}: {
  profile: Profile
}) {
  let { data: publications } = usePublications({
    where: {
      publicationTypes: [PublicationType.Post],
      from: [profile.id],
    },
    limit: LimitType.TwentyFive
  })

  return (
    <>
      {
        publications?.map((pub: any, index: number) => (
          <div key={index} className="py-4 bg-zinc-900 rounded mb-3 px-4">
            <p>{pub.metadata.content}</p>
            {
              pub.metadata?.asset?.image?.optimized?.uri && (
                <img
                  width="400"
                  height="400"
                  alt={profile.handle?.fullHandle}
                  className='rounded-xl mt-6 mb-2'
                  src={pub.metadata?.asset?.image?.optimized?.uri}
                />
              )
            }
          </div>
        ))
    }
    </>
  )
}

What's happening

useProfile allows you to get a user's profile details by passing in a Lens handle or profile ID

usePublications allows you to fetch a user's publications by passing in a profile

Testing it out

To run the app, run the following command:

npm run dev

Adding authentication and following a user

Next, let's add some additional functionality that will allow a user to sign and and then follow another user.

// app/profile/[handle]/page.tsx
'use client'
import {
  useProfile,
  usePublications,
  Profile,
  LimitType,
  PublicationType,
  useLogin,
  useProfiles,
  useFollow
} from '@lens-protocol/react-web'
import { useWeb3Modal } from '@web3modal/wagmi/react'
import { useAccount } from 'wagmi'

export default function Profile({ params: { handle }}) {
  const namespace = handle.split('.')[1]
  handle = handle.split('.')[0]
  let { data: profile, loading } = useProfile({
    forHandle: `${namespace}/${handle}`
  })
  const { open } = useWeb3Modal()
  const { address, isConnected } = useAccount()
  const { execute: login, data } = useLogin()
  const { execute: follow } = useFollow();
  const { data: ownedProfiles } = useProfiles({
    where: {
      ownedBy: [address || ''],
    },
  })
  
  if (!profile) return null

  if (loading) return <p className="p-14">Loading ...</p>
  return (
    <div>
      <div className="p-14">
        {
          profile?.metadata?.picture?.__typename === 'ImageSet' && (
            <img
              width="200"
              height="200"
              alt={profile.handle?.fullHandle}
              className='rounded-xl'
              src={profile.metadata.picture.optimized?.uri}
            />
          )
        }
        {
          !isConnected && (
            <button
              className='border border-zinc-600 rounded px-4 py-2 mt-4 mb-6'
              onClick={() => open()}>
                Connect Wallet
            </button>
          )
        }
        {
          !data && ownedProfiles && isConnected && (
            <button
              className='border border-zinc-600 rounded px-4 py-2 mt-4 mb-6'
              onClick={() => login({
                address: address || '',
                profileId: ownedProfiles[ownedProfiles.length - 1].id
              })}>
                Login with Lens
            </button>
          )
        }
        {
         data && profile.operations.canFollow !== 'NO' && (
            <button
            className='border border-zinc-600 rounded px-4 py-2 mt-4 mb-6'
            onClick={() => profile ? follow({ profile: profile }) : null }>
              Follow
            </button>
          )
        }
        <h1 className="text-3xl my-3">{profile?.handle?.localName}.{profile?.handle?.namespace}</h1>
        <h3 className="text-xl mb-4">{profile?.metadata?.bio}</h3>
       { profile && <Publications profile={profile} />}
      </div>
    </div>
  )
}

function Publications({
  profile
}: {
  profile: Profile
}) {
  let { data: publications } = usePublications({
    where: {
      publicationTypes: [PublicationType.Post],
      from: [profile.id],
    },
    limit: LimitType.TwentyFive
  })

  return (
    <>
      {
        publications?.map((pub: any, index: number) => (
          <div key={index} className="py-4 bg-zinc-900 rounded mb-3 px-4">
            <p>{pub.metadata.content}</p>
            {
              pub.metadata?.asset?.image?.optimized?.uri && (
                <img
                  width="400"
                  height="400"
                  alt={profile.handle?.fullHandle}
                  className='rounded-xl mt-6 mb-2'
                  src={pub.metadata?.asset?.image?.optimized?.uri}
                />
              )
            }
          </div>
        ))
    }
    </>
  )
}

When you run the app, you should now be able to sign in and follow another user.

Next Steps

Now that you've built your first basic application, it's time to explore more of the Lens API!

Consider diving into modules or learning about gasless transactions and the dispatcher.

Also consider adding the following features to your new app:

  • Searching for users
  • Creating a post

About

Introduction to web3 social media with Next.js and Lens Protocol

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published