第十周 使用Lens创建一个去中心化的推特
官方资料 https://docs.alchemy.com/docs/how-to-create-a-decentralized-twitter-with-lens-protocol
最后更新于
官方资料 https://docs.alchemy.com/docs/how-to-create-a-decentralized-twitter-with-lens-protocol
最后更新于
关注我的推特(https://twitter.com/SoullessL) ,及时获取Alchemy 教程的最新信息。
进入https://replit.com/,如果没有账号,可以注册一个
点击创建一个Node.js的项目
创建成功以后在shell里输入如下代码,其中road-to-lens为你的项目名字,可以随意
npx create-next-app road-to-lens
输入成功以后,提示 'Ok to processed',输入y,等待下载项目所需的安装包并创建项目。
然后我们在Shell里输入 cd road-to-lens 切换到项目所在路径下。
因为我们的项目是在一个子文件夹下面的,所以我们需要修改启动项
首先显示隐藏文件
然后我们打开.replit文件,删除原有内容,然后添加下面的两行内容,其中road-to-lens为你创建项目的名称
run = 'npm --prefix ./road-to-lens run dev'
entrypoint = './road-to-lens/pages/index.jsx'
然后我们点击“Run” 按钮,最终出现如上界面就是项目编译成功
然后我们回到Shell里,输入命令 npm install @apollo/client graphql 安装graphql包。
然后我们回到road-to-lens文件夹下面,右键Add File,创建一个叫apollo-client.js的文件,并且在文件里面添加以下代码。
// ./apollo-client.js
import { ApolloClient, InMemoryCache } from "@apollo/client";
const client = new ApolloClient({
uri: "https://api.lens.dev",
cache: new InMemoryCache(),
});
export default client;
然后我们找到pages/_app.js文件,用以下代码替换文件内容
// pages/_app.js
import '../styles/globals.css'
import { ApolloProvider } from "@apollo/client";
import client from "../apollo-client";
function MyApp({ Component, pageProps }) {
return (
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp
再找到pages/index.js,用以下代码替换文件内容
import { useQuery, gql } from "@apollo/client";
const recommendProfiles = gql`
query RecommendedProfiles {
recommendedProfiles {
id
name
bio
attributes {
displayType
traitType
key
value
}
followNftAddress
metadata
isDefault
picture {
... on NftImage {
contractAddress
tokenId
uri
verified
}
... on MediaSet {
original {
url
mimeType
}
}
__typename
}
handle
coverPicture {
... on NftImage {
contractAddress
tokenId
uri
verified
}
... on MediaSet {
original {
url
mimeType
}
}
__typename
}
ownedBy
dispatcher {
address
canUseRelay
}
stats {
totalFollowers
totalFollowing
totalPosts
totalComments
totalMirrors
totalPublications
totalCollects
}
followModule {
... on FeeFollowModuleSettings {
type
amount {
asset {
symbol
name
decimals
address
}
value
}
recipient
}
... on ProfileFollowModuleSettings {
type
}
... on RevertFollowModuleSettings {
type
}
}
}
}
`;
export default function Home() {
const {loading, error, data} = useQuery(recommendProfiles);
if (loading) return 'Loading..';
if (error) return `Error! ${error.message}`;
return (
<div>
Hello
{data.recommendedProfiles.map((profile, index) => {
console.log(`Profile ${index}:`, profile);
return (
<div>
<h1>{profile.name}</h1>
<p>{profile.bio}</p>
<div>{profile.attributes.map((attr, idx) => {
if (attr.key === "website") {
return <div><a href={`${attr.value}`}>{attr.value}</a><br/></div>
} else if (attr.key === "twitter") {
return <div><a href={`https://twitter.com/${attr.value}`}>@{attr.value}</a><br/></div>;
}
return(<div>{attr.value}</div>);
})}</div>
</div>
);
})}
</div>
)
}
然后我们回到road-to-lens下面,新建一个queries文件夹,并在下面新建一个recommendedProfilesQuery.js文件,然后在recommendedProfilesQuery.js下面添加如下代码。
// queries/recommendedProfilesQuery.js
import {gql} from '@apollo/client';
export default gql`
query RecommendedProfiles {
recommendedProfiles {
id
name
bio
attributes {
displayType
traitType
key
value
}
followNftAddress
metadata
isDefault
picture {
... on NftImage {
contractAddress
tokenId
uri
verified
}
... on MediaSet {
original {
url
mimeType
}
}
__typename
}
handle
coverPicture {
... on NftImage {
contractAddress
tokenId
uri
verified
}
... on MediaSet {
original {
url
mimeType
}
}
__typename
}
ownedBy
dispatcher {
address
canUseRelay
}
stats {
totalFollowers
totalFollowing
totalPosts
totalComments
totalMirrors
totalPublications
totalCollects
}
followModule {
... on FeeFollowModuleSettings {
type
amount {
asset {
symbol
name
decimals
address
}
value
}
recipient
}
... on ProfileFollowModuleSettings {
type
}
... on RevertFollowModuleSettings {
type
}
}
}
}
`;
然后我们在road-to-lens文件夹下面,新建components文件夹,并且在文件夹下面新建一个名为Profile.js的文件,然后把下面的代码复制到文件里。
// components/Profile.js
import Link from "next/link";
export default function Profile(props) {
const profile = props.profile;
// When displayFullProfile is true, we show more info.
const displayFullProfile = props.displayFullProfile;
return (
<div className="p-8">
<Link href={`/profile/${profile.id}`}>
<div className="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
<div className="md:flex">
<div className="md:shrink-0">
{profile.picture ? (
<img
src={
profile.picture.original
? profile.picture.original.url
: profile.picture.uri
}
className="h-48 w-full object-cover md:h-full md:w-48"
/>
) : (
<div
style={{
backgrondColor: "gray",
}}
className="h-48 w-full object-cover md:h-full md:w-48"
/>
)}
</div>
<div className="p-8">
<div className="uppercase tracking-wide text-sm text-indigo-500 font-semibold">
{profile.handle}
{displayFullProfile &&
profile.name &&
" (" + profile.name + ")"}
</div>
<div className="block mt-1 text-sm leading-tight font-medium text-black hover:underline">
{profile.bio}
</div>
<div className="mt-2 text-sm text-slate-900">{profile.ownedBy}</div>
<p className="mt-2 text-xs text-slate-500">
following: {profile.stats.totalFollowing} followers:{" "}
{profile.stats.totalFollowers}
</p>
</div>
</div>
</div>
</Link>
</div>
);
}
然后我们修改pages/index.js文件,把里面的内容替换为如下内容
import { useQuery, gql } from "@apollo/client";
import recommendedProfilesQuery from '../queries/recommendedProfilesQuery.js';
import Profile from '../components/Profile.js';
export default function Home() {
const {loading, error, data} = useQuery(recommendedProfilesQuery);
if (loading) return 'Loading..';
if (error) return `Error! ${error.message}`;
return (
<div>
{data.recommendedProfiles?.map((profile, index) => {
console.log(`Profile ${index}:`, profile);
return <Profile key={profile.id} profile={profile} displayFullProfile={false} />;
})}
</div>
)
}
然后点击页面上面绿色的Run按钮把项目跑起来,预览大概如图所示,会有一些lens的数据,但是还没有样式。
然后我们回到Shell终端,输入下面的命令安装样式库 tailwindcss
npm install -D tailwindcss postcss autoprefixer
安装完再输入如下命令初始化tailwindcss
npx tailwindcss init -p
然后我们进入road-to-lens根目录,找到文件 tailwind.config.js,用以下的内容替换文件内容
// tailwind.config.js
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
接着我们找到styles/globals.css文件,用以下内容替换文件内容。
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}
/* ./styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
然后我们的界面大概是这样的。
然后我们再pages下面新建一个profile文件夹,然后在下面新建一个[id].js文件,这个页面用来显示用户的详情页,并在文件里添加如下内容
import { useQuery, useMutation } from "@apollo/client";
import { useRouter } from "next/router";
import fetchProfileQuery from "../../queries/fetchProfileQuery.js";
import Profile from "../../components/Profile.js";
import Post from "../../components/Post.js";
export default function ProfilePage() {
const router = useRouter();
const { id } = router.query;
console.log("fetching profile for", id);
const { loading, error, data } = useQuery(fetchProfileQuery, {
variables: {
request: { profileId: id },
publicationsRequest: {
profileId: id,
publicationTypes: ["POST"],
},
},
});
if (loading) return "Loading..";
if (error) return `Error! ${error.message}`;
return (
<div className="flex flex-col p-8 items-center">
<Profile profile={data.profile} displayFullProfile={true} />
{data.publications.items.map((post, idx) => {
return <Post key={idx} post={post}/>;
})}
</div>
);
}
然后我们在queries文件夹下面新增文件fetchProfileQuery.js,这个用来获取用户详情数据,并在文件里添加如下内容
import { gql } from "@apollo/client";
export default gql`
query (
$request: SingleProfileQueryRequest!
$publicationsRequest: PublicationsQueryRequest!
) {
publications( request: $publicationsRequest) {
items {
__typename
... on Post {
...PostFields
}
... on Comment {
...CommentFields
}
... on Mirror {
...MirrorFields
}
}
pageInfo {
prev
next
totalCount
}
}
profile(request: $request) {
id
name
bio
attributes {
displayType
traitType
key
value
}
followNftAddress
metadata
isDefault
picture {
... on NftImage {
contractAddress
tokenId
uri
verified
}
... on MediaSet {
original {
url
mimeType
}
}
__typename
}
handle
coverPicture {
... on NftImage {
contractAddress
tokenId
uri
verified
}
... on MediaSet {
original {
url
mimeType
}
}
__typename
}
ownedBy
dispatcher {
address
canUseRelay
}
stats {
totalFollowers
totalFollowing
totalPosts
totalComments
totalMirrors
totalPublications
totalCollects
}
followModule {
... on FeeFollowModuleSettings {
type
amount {
asset {
symbol
name
decimals
address
}
value
}
recipient
}
... on ProfileFollowModuleSettings {
type
}
... on RevertFollowModuleSettings {
type
}
}
}
}
fragment MediaFields on Media {
url
mimeType
}
fragment ProfileFields on Profile {
id
name
bio
attributes {
displayType
traitType
key
value
}
isFollowedByMe
isFollowing(who: null)
followNftAddress
metadata
isDefault
handle
picture {
... on NftImage {
contractAddress
tokenId
uri
verified
}
... on MediaSet {
original {
...MediaFields
}
}
}
coverPicture {
... on NftImage {
contractAddress
tokenId
uri
verified
}
... on MediaSet {
original {
...MediaFields
}
}
}
ownedBy
dispatcher {
address
}
stats {
totalFollowers
totalFollowing
totalPosts
totalComments
totalMirrors
totalPublications
totalCollects
}
followModule {
... on FeeFollowModuleSettings {
type
amount {
asset {
name
symbol
decimals
address
}
value
}
recipient
}
... on ProfileFollowModuleSettings {
type
}
... on RevertFollowModuleSettings {
type
}
}
}
fragment PublicationStatsFields on PublicationStats {
totalAmountOfMirrors
totalAmountOfCollects
totalAmountOfComments
}
fragment MetadataOutputFields on MetadataOutput {
name
description
content
media {
original {
...MediaFields
}
}
attributes {
displayType
traitType
value
}
}
fragment Erc20Fields on Erc20 {
name
symbol
decimals
address
}
fragment CollectModuleFields on CollectModule {
__typename
... on FreeCollectModuleSettings {
type
followerOnly
contractAddress
}
... on FeeCollectModuleSettings {
type
amount {
asset {
...Erc20Fields
}
value
}
recipient
referralFee
}
... on LimitedFeeCollectModuleSettings {
type
collectLimit
amount {
asset {
...Erc20Fields
}
value
}
recipient
referralFee
}
... on LimitedTimedFeeCollectModuleSettings {
type
collectLimit
amount {
asset {
...Erc20Fields
}
value
}
recipient
referralFee
endTimestamp
}
... on RevertCollectModuleSettings {
type
}
... on TimedFeeCollectModuleSettings {
type
amount {
asset {
...Erc20Fields
}
value
}
recipient
referralFee
endTimestamp
}
}
fragment PostFields on Post {
id
profile {
...ProfileFields
}
stats {
...PublicationStatsFields
}
metadata {
...MetadataOutputFields
}
createdAt
collectModule {
...CollectModuleFields
}
referenceModule {
... on FollowOnlyReferenceModuleSettings {
type
}
}
appId
hidden
mirrors(by: null)
hasCollectedByMe
}
fragment MirrorBaseFields on Mirror {
id
profile {
...ProfileFields
}
stats {
...PublicationStatsFields
}
metadata {
...MetadataOutputFields
}
createdAt
collectModule {
...CollectModuleFields
}
referenceModule {
... on FollowOnlyReferenceModuleSettings {
type
}
}
appId
hidden
hasCollectedByMe
}
fragment MirrorFields on Mirror {
...MirrorBaseFields
mirrorOf {
... on Post {
...PostFields
}
... on Comment {
...CommentFields
}
}
}
fragment CommentBaseFields on Comment {
id
profile {
...ProfileFields
}
stats {
...PublicationStatsFields
}
metadata {
...MetadataOutputFields
}
createdAt
collectModule {
...CollectModuleFields
}
referenceModule {
... on FollowOnlyReferenceModuleSettings {
type
}
}
appId
hidden
mirrors(by: null)
hasCollectedByMe
}
fragment CommentFields on Comment {
...CommentBaseFields
mainPost {
... on Post {
...PostFields
}
... on Mirror {
...MirrorBaseFields
mirrorOf {
... on Post {
...PostFields
}
... on Comment {
...CommentMirrorOfFields
}
}
}
}
}
fragment CommentMirrorOfFields on Comment {
...CommentBaseFields
mainPost {
... on Post {
...PostFields
}
... on Mirror {
...MirrorBaseFields
}
}
}
`;
然后我们在components下面新建一个Post.js,并添加如下内容到文件中。
// components/Post.js
export default function Post(props) {
const post = props.post;
return (
<div className="p-8">
<div className="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
<div className="md:flex">
<div className="p-8">
<p className="mt-2 text-xs text-slate-500 whitespace-pre-line">
{post.metadata.content}
</p>
</div>
</div>
</div>
</div>
);
}
如果出现这个界面,则说明我们样式也加好了。有时候因为这个在线版本的开发工具会有点卡,导致界面加载不出来,你可以把网站关了,过个十几分钟再过来试一下,可能就好了。
然后我们点击上面截图里右上角那个按钮,可以重新打开一个窗口来预览我们的项目。
我们可以点击某个lens用户,进入详情页。
然后你可以点击publish来把项目发布出去,具体的流程可以参考第四周最后的教程,记住你发布以后的网址,在填表的时候使用。
填表地址 https://alchemyapi.typeform.com/roadtoweekten 这个是老地址已经关闭了,不确定以后会不会再开。
另外NFT领取地址等待快照(快照时间不固定),然后上 https://mintkudos.xyz/ 连接进钱包,进入profile,会有一个to claim菜单,就是可以领取的NFT,如果没有则说明还没有快照。