Skip to content
This repository was archived by the owner on Aug 27, 2025. It is now read-only.

Commit 9f6609a

Browse files
committed
feat: following button
1 parent 5923edd commit 9f6609a

5 files changed

Lines changed: 203 additions & 10 deletions

File tree

src/components/FollowingButton.tsx

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { useRouter } from "next/router"
2+
import { Button } from "~/components/ui/Button"
3+
import type { Variant } from "~/components/ui/Button"
4+
import { useAccount } from "wagmi"
5+
import { useEffect, useState } from "react"
6+
import {
7+
useGetLinks,
8+
useLinkCharacter,
9+
useUnlinkCharacter,
10+
useGetCharacters,
11+
} from "~/queries/character"
12+
import { useConnectModal } from "@rainbow-me/rainbowkit"
13+
import clsx from "clsx"
14+
import { BellIcon } from "@heroicons/react/24/solid"
15+
16+
export const FollowingButton: React.FC<{
17+
characterId?: number
18+
variant?: Variant
19+
className?: string
20+
size?: "sm" | "xl"
21+
}> = ({ characterId, variant, className, size }) => {
22+
const { address } = useAccount()
23+
const linkCharacter = useLinkCharacter()
24+
const unlinkCharacter = useUnlinkCharacter()
25+
const { openConnectModal } = useConnectModal()
26+
const [followProgress, setFollowProgress] = useState<boolean>(false)
27+
const userCharacters = useGetCharacters(address, true)
28+
const router = useRouter()
29+
30+
const backlinks = useGetLinks(
31+
userCharacters.data?.list[0].characterId,
32+
characterId,
33+
)
34+
35+
const handleClickSubscribe = async (e: any) => {
36+
e.preventDefault()
37+
if (!address) {
38+
setFollowProgress(true)
39+
openConnectModal?.()
40+
} else if (!userCharacters.data?.count) {
41+
router.push("/new")
42+
} else if (characterId) {
43+
if (backlinks.data?.count) {
44+
unlinkCharacter.mutate({
45+
fromCharacterId: userCharacters.data?.list[0].characterId,
46+
toCharacterId: characterId,
47+
})
48+
} else {
49+
linkCharacter.mutate({
50+
fromCharacterId: userCharacters.data?.list[0].characterId,
51+
toCharacterId: characterId,
52+
})
53+
}
54+
}
55+
}
56+
57+
useEffect(() => {
58+
if (
59+
followProgress &&
60+
address &&
61+
backlinks.isSuccess &&
62+
characterId &&
63+
userCharacters.isSuccess
64+
) {
65+
if (!userCharacters.data?.count) {
66+
router.push("/new")
67+
}
68+
if (!backlinks.data?.count) {
69+
linkCharacter.mutate({
70+
fromCharacterId: userCharacters.data?.list[0].characterId,
71+
toCharacterId: characterId,
72+
})
73+
}
74+
setFollowProgress(false)
75+
}
76+
}, [
77+
backlinks.isSuccess,
78+
backlinks.data?.count,
79+
router,
80+
followProgress,
81+
address,
82+
userCharacters.isSuccess,
83+
userCharacters.data,
84+
characterId,
85+
linkCharacter,
86+
])
87+
88+
return (
89+
<Button
90+
variant={variant}
91+
onClick={handleClickSubscribe}
92+
className={clsx(className, "align-middle space-x-1")}
93+
isLoading={
94+
backlinks.data?.count
95+
? linkCharacter.isLoading || unlinkCharacter.isLoading
96+
: userCharacters.isLoading ||
97+
unlinkCharacter.isLoading ||
98+
linkCharacter.isLoading ||
99+
backlinks.isLoading
100+
}
101+
size={size}
102+
aria-label="follow"
103+
>
104+
<BellIcon className="h-4 w-4" />
105+
{backlinks.data?.count ? (
106+
<>
107+
<span className="group-hover:hidden">Following</span>
108+
<span className="hidden group-hover:block">Unfollow</span>
109+
</>
110+
) : (
111+
<span>Follow</span>
112+
)}
113+
</Button>
114+
)
115+
}

src/components/ui/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const Button = React.forwardRef<
5454
disabled={isDisabled || isLoading}
5555
className={clsx(
5656
className,
57-
"button",
57+
"button shadow",
5858
isLoading && "is-loading",
5959
isBlock && `is-block`,
6060
variantColor && `is-${variantColor}`,

src/models/character.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { indexer } from "../lib/crossbell"
1+
import { indexer } from "~/lib/crossbell"
22
import { Notes, Note } from "~/lib/types"
3+
import type { Contract } from "crossbell.js"
34

45
const expandPage = async (note: Note) => {
56
note.cover = note?.metadata?.content?.attachments?.find((attachment) =>
@@ -75,9 +76,10 @@ export const getAchievements = (characterId: number) => {
7576
})
7677
}
7778

78-
export const getCharacters = async (address: string) => {
79+
export const getCharacters = async (address: string, primary?: boolean) => {
7980
const result = await indexer.getCharacters(address, {
8081
limit: 50,
82+
primary,
8183
})
8284
result.list = result.list.sort((a, b) => {
8385
if (a.primary) {
@@ -90,7 +92,28 @@ export const getCharacters = async (address: string) => {
9092
return -1
9193
}
9294
})
93-
console.log(result)
9495

9596
return result
9697
}
98+
99+
export const getLinks = (characterId: number, toCharacterId: number) => {
100+
return indexer.getLinks(characterId, {
101+
toCharacterId: toCharacterId,
102+
})
103+
}
104+
105+
export const linkCharacter = (
106+
contract: Contract,
107+
fromCharacterId: number,
108+
toCharacterId: number,
109+
) => {
110+
return contract.linkCharacter(fromCharacterId, toCharacterId, "follow")
111+
}
112+
113+
export const unlinkCharacter = (
114+
contract: Contract,
115+
fromCharacterId: number,
116+
toCharacterId: number,
117+
) => {
118+
return contract.unlinkCharacter(fromCharacterId, toCharacterId, "follow")
119+
}

src/pages/[handle].tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { toGateway } from "~/lib/ipfs-parser"
3131
import { Platform } from "~/components/Platform"
3232
import { Source } from "~/components/Source"
3333
import { Button } from "~/components/ui/Button"
34+
import { FollowingButton } from "~/components/FollowingButton"
3435

3536
dayjs.extend(duration)
3637
dayjs.extend(relativeTime)
@@ -129,8 +130,11 @@ export default function HandlePage() {
129130
>
130131
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-br from-white to-gray-200 opacity-80"></div>
131132
<div className="flex relative">
132-
<div className="absolute right-0 top-0 bg-blue-400 text-white px-12 py-1 rounded-2xl cursor-pointer">
133-
Follow
133+
<div className="absolute right-0 top-0">
134+
<FollowingButton
135+
className="rounded-full"
136+
characterId={character.data?.characterId}
137+
/>
134138
</div>
135139
<div className="w-32 text-center mr-4 flex flex-col items-center justify-between">
136140
{character.data?.metadata?.content?.avatars && (
@@ -148,7 +152,7 @@ export default function HandlePage() {
148152
<div className="flex-1 min-w-0">
149153
<p className="font-medium text-2xl">
150154
<span>{character.data?.metadata?.content?.name}</span>
151-
<span className="text-base ml-2">@{handle}</span>
155+
<span className="text-base ml-2 text-zinc-500">@{handle}</span>
152156
</p>
153157
<p className="truncate text-sm mt-1">
154158
{character.data?.metadata?.content?.bio}

src/queries/character.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useInfiniteQuery,
66
} from "@tanstack/react-query"
77
import * as characterModel from "../models/character"
8+
import { useContract } from "~/lib/crossbell"
89

910
export const useGetCharacter = (handle: string) => {
1011
return useQuery(["getCharacter", handle], async () => {
@@ -74,11 +75,61 @@ export const useGetCalendar = (characterId: number) => {
7475
})
7576
}
7677

77-
export const useGetCharacters = (address?: string) => {
78-
return useQuery(["getCharacters", address], async () => {
78+
export const useGetCharacters = (address?: string, primary?: boolean) => {
79+
return useQuery(["getCharacters", address, primary], async () => {
7980
if (!address) {
8081
return null
8182
}
82-
return characterModel.getCharacters(address)
83+
return characterModel.getCharacters(address, primary)
8384
})
8485
}
86+
87+
export const useGetLinks = (characterId?: number, toCharacterId?: number) => {
88+
return useQuery(["getLinks", characterId, toCharacterId], async () => {
89+
if (!characterId || !toCharacterId) {
90+
return null
91+
}
92+
return characterModel.getLinks(characterId, toCharacterId)
93+
})
94+
}
95+
96+
export const useLinkCharacter = () => {
97+
const contract = useContract()
98+
const queryClient = useQueryClient()
99+
return useMutation(
100+
async (input: { fromCharacterId?: number; toCharacterId?: number }) => {
101+
if (!input.fromCharacterId || !input.toCharacterId) {
102+
return null
103+
}
104+
return characterModel.linkCharacter(
105+
contract,
106+
input.fromCharacterId,
107+
input.toCharacterId,
108+
)
109+
},
110+
{
111+
onSuccess: (data, variables) => {
112+
queryClient.invalidateQueries(["getLinks", variables.fromCharacterId])
113+
},
114+
},
115+
)
116+
}
117+
118+
export const useUnlinkCharacter = () => {
119+
const contract = useContract()
120+
const queryClient = useQueryClient()
121+
return useMutation(
122+
async (input: { fromCharacterId: number; toCharacterId: number }) => {
123+
return characterModel.unlinkCharacter(
124+
contract,
125+
input.fromCharacterId,
126+
input.toCharacterId,
127+
)
128+
},
129+
{
130+
onSuccess: (data, variables) => {
131+
queryClient.invalidateQueries(["getLinks", variables.fromCharacterId])
132+
},
133+
},
134+
)
135+
}

0 commit comments

Comments
 (0)